Turfi Platform Documentation
Official Turfi documentation portal for users, admins, and developers.
Documentation Search
Search only within Turfi documentation pages.
Import System
Bulk ingestion architecture, registry resolution rules, lookup-backed metadata handling, and import execution behavior.
Bulk data ingestion is one of the core operational needs of the platform, because leagues, clubs, and administrators cannot maintain Turfi one row at a time. This document explains how import workflows translate external spreadsheets into governed platform records. It fits into the architecture as the bridge between external operational data and Turfi's internal registry, player, and competition systems. As import capabilities expand, this file should remain the reference for resolver behavior, performance strategy, and ingestion rules.
Shared status legend: [docs/_shared/status-legend.md](./_shared/status-legend.md)
Import Engine
Purpose
The Turfi Import Engine allows administrators to import structured datasets into the platform using CSV files or League Packages.
The engine is designed to support large scale data ingestion such as importing entire leagues, tournaments, or academy structures.
Location in application
Admin Imports
Navigation structure
Admin Imports New Import Import Jobs Import Templates
Import types
The engine supports two import methods.
CSV Import
Allows importing individual entity datasets such as players, teams, clubs, competitions, seasons, venues, turfs, businesses, addresses, contacts, and roster registrations.
Users upload a CSV file and map columns to platform fields before executing the import.
League Package Import
Allows importing an entire league structure using a single archive file.
A League Package typically contains multiple files including:
league.jsonclubs.csvteams.csvplayers.csvrosters.csvgames.csv
The platform processes these files in dependency order to construct the full competition structure.
Import processing pipeline
All imports follow the same processing pipeline.
Upload Parse Normalize Validate Preview Execute Report
Detailed flow:
- Upload CSV (or package file) in Admin Imports.
- Parse source columns and rows.
- Map source columns to destination fields.
- Validate required fields and detect obvious issues.
- Preview transformed rows.
- Run import adapter per entity.
- Persist job and row results for audit/review.
During preview the system highlights:
- Missing required fields
- Reference mismatches
- Potential duplicates
After execution the engine produces an import report summarizing:
- Total rows processed
- Successful rows
- Failed rows
- Warnings
Import Architecture
All imports run through a shared import service.
The import service delegates row processing to entity specific adapters such as:
- player import adapter
- team import adapter
- club import adapter
- competition import adapter
- season import adapter
- venue import adapter
- turf import adapter
- business import adapter
- address import adapter
- contact import adapter
- player registration import adapter
This architecture ensures that new entities can be added to the import engine without modifying the core import system.
Import Tracking
Import jobs are recorded through API-facing views and underlying system tables.
Primary API views used by the admin interface:
api_import_jobsapi_import_rowsapi_import_mappingsapi_import_entity_adapters
Underlying tables used for persistence and execution tracking include:
import_jobsimport_rowsimport_mappingsimport_entity_adaptersimport_reference_cacheimport_job_files
These tables allow administrators to inspect previous imports and diagnose failures.
Key tracking behavior:
api_import_jobsexposes job-level status, counts, and timingapi_import_rowsexposes row-level outcomes (success/warning/failed)api_import_mappingsstores reusable column mapping templatesapi_import_entity_adaptersdefines destination/required field metadata per entity
Relationship to Data Management Interface
The Import Engine is designed to complement the Admin Data Management Interface.
Bulk data can be imported using the Import Engine while individual records can still be managed manually through the Admin Data pages.
Together these systems provide a complete solution for both large scale and small scale data management within the Turfi platform.
Smart Reference Mapping in the Import Engine
Purpose
Smart Reference Mapping removes the need for administrators to provide internal UUIDs in CSV files for common relationship fields.
Users can provide human-readable values such as:
teamclubvenuecompetitionseason
instead of only:
team_idclub_idvenue_idcompetition_idseason_id
Architecture Overview
The Import Engine resolves references during row processing by combining:
- field mapping metadata from import adapters
- entity resolver lookup logic
- per-job resolution cache and exception mapping
If a row contains a relationship name but no ID, the import pipeline attempts to resolve that value before running entity adapters.
Database Model
Core tables/views in this workflow:
api_import_entity_adaptersapi_import_jobsapi_import_rowsimport_jobsimport_rowsimport_mappingsimport_reference_cache
import_reference_cache supports "resolve once, apply everywhere" behavior when the same source value appears repeatedly.
Workflow
- Upload CSV
- Configure field mapping
- Auto-match reference values
- Resolve exceptions (if ambiguous/not found)
- Preview transformed rows
- Choose import mode
- Execute import
- Review job and row outcomes
Resolve Exceptions
When the resolver cannot confidently match values (for example CF Montreal vs Montreal CF), the import flow must surface those rows for manual resolution.
Once a user picks the correct entity, the mapping is reused across all matching rows in the same import job.
Import Modes
- Insert only
- Update existing
- Upsert
Duplicate creation should never be the default platform behavior.
Import System
Status: IMPLEMENTED
The Import System is how large amounts of data get into Turfi. When a league uploads a roster CSV, when an admin migrates historical games, or when a club bulk-loads venues and turfs, the Import System handles the upload, mapping, validation, and execution. It turns spreadsheets and League Packages into structured records that the rest of the platform can use.
Manual data entry doesn't scale. A single competition might have hundreds of players and dozens of games; federations may need to backfill years of history. The Import System exists so admins can use the tools they already have (CSV, Excel) and get data into Turfi without typing each record by hand. It also insulates the platform from requiring internal UUIDs—names and references are resolved automatically.
This system owns CSV and League Package upload and parsing, field mapping (source columns to destination fields), entity resolution (matching keys, slugs, and names to internal IDs), preview validation and row-level error reporting, import job execution and tracking, and adapters for each entity type (players, teams, clubs, competitions, competition registrations, etc.).
Import feeds the Player Engine, Competition Engine, and Admin Data registries. Registry entities use database-enforced identity generation for key and slug behavior, while player imports continue to rely on the separate Player Identity infrastructure, resolver views, duplicate detection, and merge workflows. The Infrastructure Engine provides transactional execution; Admin Data provides the UI entry point and post-import review.
Future work includes pre-import duplicate warnings, smarter mapping suggestions, incremental and delta imports, and integration with federation data feeds for automated league sync.
Purpose
The Import System enables administrators to import structured sports data into Turfi using CSV-based workflows. It supports operational scenarios including large scale data ingestion, league roster imports, historical backfills, and admin data migration.
The system is designed so import files do not need internal UUIDs. Administrators can provide _key values as the preferred registry reference format, then slug or name when needed, and the platform resolves those values to internal IDs during processing.
Architecture Overview
The Import System is implemented as a staged pipeline:
- CSV upload
- field mapping
- automatic entity resolution
- exception resolution
- preview validation
- import execution
Current implementation is split across:
- Admin Import UI (
/admin/imports/new,/admin/imports/jobs,/admin/imports/templates) - shared import orchestration service (
lib/imports/importService.ts) - entity adapters (
player,team,club,competition,season,venue,turf,business,address,contact,player_registration) - shared resolver service (
lib/entityResolver.ts)
For registry entities (organizations, leagues, seasons, competitions, clubs, teams, venues, turfs, businesses, addresses, contacts) the resolver follows key -> slug -> normalized_name -> name fallback when those entities themselves are being created or updated. organizations, clubs, and leagues use database-maintained canonical names for that name-based match step. Players remain on the existing Player Engine identity and resolver flow rather than the generic registry identity path, even though they now participate in the standardized registry lifecycle model through status.
Normalized category lookups used by imports:
leagues.gender_idcompetitions.age_group_idcompetitions.gender_idteams.age_group_idteams.gender_idplayers.gender_id
The resolver translates import relationships into foreign keys before adapter writes, for example:
team_key->team_idclub_key->club_idcompetition_key->competition_idowner_organization_key->owner_organization_idvenue_key->venue_id
This prevents import users from needing to know internal identifiers ahead of time.
Key Database Models
Current import execution and tracking model:
import_jobsimport_rowsimport_mappingsimport_entity_adaptersimport_job_filesimport_reference_cacheapi_import_jobsapi_import_rowsapi_import_mappingsapi_import_entity_adapters
Legacy/compatibility import model tables that may exist in some schema snapshots:
player_data_importsimported_gamesimported_media
Reference resolution targets used during import mapping:
- Registry relationships resolved primarily through
_keyfields (organization_key,league_key,season_key,competition_key,club_key,team_key,venue_key,turf_key,business_key) - Registry identity trigger:
turfi_registry_identity()onorganizations,leagues,seasons,competitions,clubs,teams,venues,turfs,businesses,addresses,contacts - Centralized lookup labels:
ui_field_labels - Club licensing lookup:
club_license_levels - Age group lookup:
age_groups - Gender lookup:
genders
Player identity-assisted matching:
player_identity_index(with resolver views used for player match assistance in import flows)
Key Workflows
- Upload CSV file
Admin uploads a CSV for a selected entity in Admin Imports.
- Map CSV columns to platform fields
Source headers are mapped to destination fields using mapping templates and adapter metadata. Example: Player Name can be mapped into first_name and last_name transformations in the import configuration path.
- Automatic entity resolution
Registry resolvers attempt key -> slug -> normalized-name-aware name matching, while player resolution continues to use the Player Engine identity infrastructure. Example: club_key = cf-montreal -> resolved club_id.
- Resolve Exceptions
Unresolved or ambiguous values are surfaced for manual correction. Example ambiguous values: CF Montreal vs Montreal CF. Once selected, the manual mapping is reused for all matching rows in the job.
- Preview Import
The system displays transformed output and row statuses, including how many rows are valid for insert/update and which rows still contain warnings/errors.
- Choose Import Mode
Operationally exposed as insert-only, update-existing, or upsert behavior (mapped internally to adapter/service modes).
- Execute Import
The import service runs entity adapters and writes to domain tables, then records job-level and row-level outcomes for audit and retry. Registry inserts may omit key and slug, in which case the database trigger generates them. If status is omitted on registry records, the database lifecycle defaults should preserve active behavior.
External Stats and Video Import Path
Turfi's Import System also acts as the structured intake layer for provider-assisted video and stats workflows. This does not replace the Media or Match Intelligence engines; instead, it stages raw provider payloads so they can be normalized into canonical match truth.
Provider-assisted imports are expected to support:
- source video registration into
game_videos - ingestion-job tracking through
video_ingestion_jobs - raw provider event capture in
video_event_imports - event-source lineage through
game_event_sources - unresolved player review through
video_player_resolution_queue - moment generation queueing through
video_moment_generation_queue - clip generation queueing through
video_clip_generation_queue
Import workflow for provider data:
- Register or resolve the target game and source video.
- Load provider payload rows through an import adapter or provider-specific parser.
- Store raw event rows before canonicalization.
- Map provider event types into Turfi event types and assign
event_source_typesuch asprovider_import. - Resolve players, teams, and timestamps where possible.
- Route unresolved items into
video_player_resolution_queueinstead of forcing incorrect canonical data. - Approve normalized events into
game_events. - Enqueue approved events for moment generation and downstream clip extraction.
This keeps the Import System aligned with the broader rule that canonical truth is always approved game_events, never raw provider feeds.
Entity Address and Contact Import Rules
The current import model distinguishes between entity-owned communication data and centralized address records.
For the core governance entities:
organizations(shown as Associations in the frontend)leaguesclubs
the import engine now supports these direct entity fields:
phoneemailwebsiteaddress_id
and these address-source fields:
address_line1address_line2cityprovincepostal_codecountry
Import behavior:
- If address fields are present, the import adapter inserts or reuses a row in
addresses - The returned UUID is written to the entity's
address_id phone,email, andwebsiteare written directly to the entity row- If address fields are missing,
address_idremainsNULL
Operational effect:
- location data is normalized in
addresses - communication data stays on the entity
- registry imports do not require operators to create address rows manually first
Additional import rules still apply:
venuesremain facility records and may import their own location and website fieldsturfsmust resolve to a venue throughvenue_key,venue_slug, orvenue_name; a turf import must not create an unlinked turfbusinessesmay resolve their linked venue throughvenue_key,venue_slug, orvenue_nameaddressesandcontactsstill import as standalone reusable records where neededcontactsimport structured person names throughfirst_nameandlast_name, while legacyfull_namefiles are still accepted and split automatically during adapter normalization
Recommended dependency order for infrastructure-oriented imports:
- organizations (Associations), leagues, clubs, and seasons where required by the broader dataset
- venues
- turfs
- businesses
- addresses
- contacts
Current contact template example:
first_name,last_name,title,email,phone,status- legacy files using
full_name,title,email,phone,statusremain compatible through import-time splitting
Import Validation and Troubleshooting
Recent registry import hardening added a few operational rules that matter when validating facility imports:
venuesmust load beforeturfs. Turf rows resolvevenue_idfrom the livevenuestable throughvenue_key,venue_slug, orvenue_name; a companion CSV is not enough if the parent venue row has not been created yet.- Inline venue address text is normalized into the shared
addressestable and linked throughentity_addresses, but the livevenuestable still expects its own base write fields such ascity,province, andcountry. Import adapters must not assume that every entity table exposes anaddress_idcolumn directly. - Blank CSV strings must be normalized before adapter writes when the destination column is typed. Common failure cases are empty strings hitting
booleancolumns such asis_indoor/is_litorintegercolumns such aslength_m/width_m. - Import preview should be treated as a resolver check, not as proof that database writes will succeed. A row can resolve every relationship correctly and still fail on table-level type or not-null constraints during execution.
- Destructive validation and wipe flows must remain manual-only. Import validation may inspect row counts, relationship coverage, and preview results, but must never trigger a data wipe implicitly.
Facility-specific execution checklist:
- import venues and confirm the expected live row count
- verify any missing venue keys before turf import
- import turfs only after every referenced
venue_keyexists live - if the registry grid does not show address data after a successful import, inspect the admin
v_*read model before changing table writes
Import Resolution System
The Import Resolution System exists so incoming files can identify entities deterministically without requiring operators to know UUIDs in advance.
Resolution order for registry entities:
keyslugname
Preferred relationship fields:
organization_keyleague_keyseason_keycompetition_keyclub_keyteam_keyvenue_keyturf_keybusiness_keyage_group_keygender_keyowner_organization_key
Example import values:
organization_key = qcslsleague_key = lsl-premierseason_key = 2025-winter
Resolution behavior:
- if key exists, reuse the UUID
- if key is missing on a new registry row, the database may generate it
- if slug is missing on a new registry row, the database may generate it
- if status is missing on a lifecycle-aware registry row, the database defaults should fall back to
active - imports must never create duplicates when a matching key already exists
Category lookup resolution rules:
gender_key->genders.key->gender_idage_group_key->age_groups.key->age_group_id- missing lookup values must not be auto-created during import
leagues,competitions,teams, andplayersmust consume the existing static lookups only
Transition rule for teams:
- imports should now treat
teams.age_group_idandteams.gender_idas the source of truth - legacy
teams.age_groupandteams.gendermay still appear temporarily for compatibility - database sync automation keeps the legacy text fields aligned during the transition
Club licensing resolution rules:
- Club licensing is optional descriptive metadata on
clubs - When an import flow surfaces licensing, it should resolve against
club_license_levels.key - Human-readable license labels must come from
ui_field_labels, not from per-lookup translation tables club_license_level_translationsmust not be used in new import architecture- Licensing must never be treated as an eligibility or federation-compliance gate during import
Import Performance Strategy
Large imports cache registry lookups so repeated references do not trigger repeated database queries.
Examples:
league_key -> league_idorganization_key -> organization_idowner_organization_key -> owner_organization_idvenue_key -> venue_idage_group_key -> age_group_idgender_key -> gender_id
This improves throughput and keeps resolution behavior consistent across large batches.
Competition Import Model
Competition imports must support the following CSV fields:
organizationleagueseasonnameage_group_keygender_keytypeformatstart_dateend_date
League is optional.
Age group and gender are optional as well. When provided, they resolve as:
age_group_key->age_groups.key->competitions.age_group_idgender_key->genders.key->competitions.gender_idtype->competition_types.key->competitions.competition_type_idformat->competition_formats.key->competitions.competition_format_id
The importer must not create missing lookup values automatically. age_group_key and gender_key must already exist in their lookup tables.
League resolution is based on name normalization rather than slug matching. Slug remains reserved for URL identity and should not be treated as the competition import identity field.
Interactions with Other Engines
- Player Engine: imports create/update player profiles, registrations, and player-linked references.
- Competition Engine: imports can create or update competitions, team registrations (
team_competitions), teams, games, and related structural data. - Media Engine: imported media metadata can seed moment/highlight-related workflows and media associations.
- Discovery Engine: imported entity records become available to search/discovery surfaces after ingestion.
- Infrastructure Engine: import jobs execute on Supabase/PostgreSQL read/write paths using API views and service-layer orchestration.
- Data Management Interface: imports complement manual Admin Data CRUD workflows for bulk operations.
Example Use Cases
- Import an annual season list before opening competition setup.
- Bulk load venue and turf inventory before publishing schedules.
- Re-import corrected club/team data with
update_onlymode. - Save mapping templates for recurring federation-provided CSV files.
Import Resolution System
Turfi imports resolve registry entities in the following order:
keyslugname
Preferred relationship fields include:
organization_keyleague_keyseason_keycompetition_keyclub_keyteam_keyvenue_keyturf_keybusiness_keyage_group_keygender_key
Resolution behavior:
- If key exists, reuse the entity UUID
- If key is missing, the entity may be created
- If slug is missing, the database generates slug
- If key is missing, the database generates key
Imports must never create duplicates when a matching key already exists.
Import Performance Strategy
Registry lookups should be cached during import execution so repeated references do not trigger repeated database queries.
Example:
league_key -> league_idage_group_key -> age_group_idgender_key -> gender_id
This keeps large import runs efficient and ensures resolution stays consistent across the same job.
Competition Import Rules
Competition CSV imports must support:
organization_keyor another supported organization referenceleague_keyor another supported league referenceseason_keyor another supported season referencenameage_group_keygender_keytypeformatstart_dateend_date
Competition import rules:
- every competition import row must resolve an organization
typeresolves throughcompetition_types.keyformatresolves throughcompetition_formats.keyleagueis optional for standalone competition types- if
type = League, the row must resolve to an existing league - if
typeisTournament,Showcase, orFriendly Series,leaguemay be blank
Age group and gender are optional, but when they are supplied they must resolve against the lookup tables:
age_group_key->age_groups.keygender_key->genders.keytype->competition_types.keyformat->competition_formats.key
League resolution must use name normalization rather than slug identity.
Example matching rule:
LOWER(name)
Example competition rows:
- League competition:
organization_key = soccer-quebec,league_key = league1-quebec,season_key = 2026-summer,name = League1 Quebec Senior Men,type = League,format = Round Robin - Standalone competition:
organization_key = soccer-quebec,league_key =,season_key = 2026-summer,name = Quebec U17 Showcase,type = Showcase,format = Group + Knockout - youth competition category example:
age_group_key = u17,gender_key = female - senior competition category example:
age_group_key = senior,gender_key = male
Centralized Address and Contact Relationship Imports
Address and contact roles are now normalized relationship metadata rather than free-text link fields.
Rules:
entity_addresses.address_role_idresolves throughaddress_roles.identity_contacts.contact_role_idresolves throughcontact_roles.id- portable import values should use lookup keys such as
address_roleandcontact_role - the import layer resolves those values through
address_roles.keyandcontact_roles.key - deprecated text columns
entity_addresses.address_roleandentity_contacts.contact_roleremain transitional only and must not be used as application write targets