Skip to content

Database Schema

Complete schema documentation for all four SQLite databases used by rondo-sync.


The rondo-sync system uses four SQLite databases to track sync state between Sportlink Club (source) and downstream systems (Laposta, Rondo Club, Nikki, FreeScout).

Critical: These databases must only exist on the production server. Running sync from a local machine creates duplicate entries because each machine tracks its own rondo_club_id mappings.

All databases are stored in the data/ directory on the server at /home/rondo/data/.

DatabasePurposeModule
laposta-sync.sqliteLaposta email list sync + Sportlink run historylib/laposta-db.js
rondo-sync.sqliteRondo Club sync (members, teams, commissies, photos, discipline, reverse sync)lib/rondo-club-db.js
nikki-sync.sqliteNikki contribution trackinglib/nikki-db.js
freescout-sync.sqliteFreeScout customer + conversation synclib/freescout-db.js

All databases use hash-based change detection to minimize API calls and avoid redundant updates.

Each trackable record has two hash fields:

  • source_hash - SHA-256 hash of current data from source system
  • last_synced_hash - Hash of data last successfully synced to destination
-- Only sync if data has changed
WHERE last_synced_hash IS NULL OR last_synced_hash != source_hash

On successful sync: last_synced_hash is updated to match source_hash

Benefits:

  • Idempotent: can re-run sync without duplicate API calls
  • Efficient: only sends changed data
  • Recoverable: failed syncs leave last_synced_hash unchanged, will retry next run

Purpose: Tracks email list synchronization between Sportlink and Laposta marketing platform.

Module: lib/laposta-db.js

Stores raw JSON results from each Sportlink Club download for audit trail.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
created_atTEXTISO timestamp of download
results_jsonTEXTComplete JSON results from Sportlink download

Purpose: Historical record of all Sportlink downloads. Used for debugging and data recovery.


Caches Laposta list custom field definitions to avoid repeated API calls.

ColumnTypeDescription
list_idTEXTLaposta list ID
field_idTEXTLaposta field ID
custom_nameTEXTField tag/name (e.g., “voornaam”)
datatypeTEXTField data type (text, numeric, select)
requiredINTEGER1 = required, 0 = optional
options_jsonTEXTJSON array of select options
updated_atTEXTLast cache refresh timestamp

Primary Key: (list_id, field_id)


Central table tracking all members across up to 4 Laposta lists.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
list_indexINTEGERWhich list: 0=LAPOSTA_LIST, 1=LIST2, 2=LIST3, 3=LIST4
list_idTEXTLaposta list ID (UUID)
emailTEXTMember email address
custom_fields_jsonTEXTJSON object of current custom field values
source_hashTEXTSHA-256 hash of email + custom_fields
last_seen_atTEXTLast time member appeared in Sportlink data
last_synced_atTEXTLast successful sync to Laposta
last_synced_hashTEXTHash of last synced data
last_synced_custom_fields_jsonTEXTPrevious custom_fields JSON (for diff display)
created_atTEXTFirst seen timestamp

Unique Constraint: (list_index, email)

Indexes: idx_members_list_hash on (list_index, source_hash, last_synced_hash)


Purpose: Tracks WordPress Rondo Club synchronization including members, parents, teams, committees, work history, photos, discipline cases, and reverse sync state.

Module: lib/rondo-club-db.js

Primary member/person records synced to WordPress.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTKNVB public person ID - UNIQUE
rondo_club_idINTEGERWordPress post ID
emailTEXTMember email address
data_jsonTEXTFull member data as JSON
source_hashTEXTSHA-256 hash of knvb_id + data
last_seen_atTEXTLast time member appeared in Sportlink data
last_synced_atTEXTLast successful sync to Rondo Club
last_synced_hashTEXTHash of last synced data
created_atTEXTFirst seen timestamp
person_image_dateTEXTDate of photo in Sportlink (change detection)
photo_stateTEXTPhoto sync state (see Photo State Machine)
photo_state_updated_atTEXTWhen photo state last changed
photo_urlTEXTPhoto download URL from MemberHeader API
photo_dateTEXTPhoto date from MemberHeader API
sync_originTEXTLast edit source: user_edit, sync_sportlink_to_rondo_club, sync_rondo_club_to_sportlink
tracked_fields_hashTEXTHash of reverse-sync tracked fields (for change detection)

Reverse sync timestamp columns (per-field modification tracking):

Column PatternFields Tracked
{field}_rondo_club_modifiedWhen the field was last modified in Rondo Club
{field}_sportlink_modifiedWhen the field was last modified in Sportlink

Tracked fields: email, email2, mobile, phone, datum_vog, freescout_id, financiele_blokkade

Indexes:

  • idx_rondo_club_members_hash on (source_hash, last_synced_hash)
  • idx_rondo_club_members_email on (email)

Critical: rondo_club_id maps KNVB ID to WordPress post ID. Without this mapping, sync creates duplicates.


Parent/guardian records (identified by email, no KNVB ID).

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
emailTEXTParent email - UNIQUE
rondo_club_idINTEGERWordPress post ID
data_jsonTEXTParent data + childKnvbIds array as JSON
source_hashTEXTSHA-256 hash of email + data
last_seen_atTEXTLast time parent appeared in Sportlink data
last_synced_atTEXTLast successful sync
last_synced_hashTEXTHash of last synced data
created_atTEXTFirst seen timestamp

Indexes: idx_rondo_club_parents_hash on (source_hash, last_synced_hash)


rondo_club_important_dates (DEPRECATED - v2.3)

Section titled “rondo_club_important_dates (DEPRECATED - v2.3)”

DEPRECATED: Birthday sync migrated to acf.birthdate on person records. Table retained for backward compatibility.

Birth dates and other important dates synced to Rondo Club.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
date_typeTEXTDate type (e.g., “birth_date”)
date_valueTEXTDate value (YYYY-MM-DD)
rondo_club_date_idINTEGERWordPress important_date post ID
source_hashTEXTSHA-256 hash
last_synced_hashTEXTHash of last synced data
last_synced_atTEXTLast successful sync
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, date_type)

Indexes: idx_rondo_club_important_dates_sync on (source_hash, last_synced_hash)


Team records from Sportlink.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
team_nameTEXTTeam name (COLLATE NOCASE)
sportlink_idTEXTSportlink team ID - UNIQUE
team_codeTEXTTeam code
rondo_club_idINTEGERWordPress team post ID
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast time team appeared in Sportlink
last_synced_atTEXTLast successful sync
last_synced_hashTEXTHash of last synced data
created_atTEXTFirst seen timestamp
game_activityTEXT”Veld” or “Zaal”
genderTEXTM/V/Mixed
player_countINTEGERNumber of players
staff_countINTEGERNumber of staff

Indexes:

  • idx_rondo_club_teams_hash on (source_hash, last_synced_hash)
  • idx_rondo_club_teams_name on (team_name COLLATE NOCASE)

Team renames: Uses sportlink_id as conflict key so renamed teams update existing WordPress posts.


Member-team assignments synced to WordPress ACF repeater.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
team_nameTEXTTeam name
rondo_club_work_history_idINTEGERWordPress work_history row index
is_backfillINTEGER1 if from historical backfill, 0 if current
source_hashTEXTSHA-256 hash
last_synced_hashTEXTHash of last synced data
last_synced_atTEXTLast successful sync
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, team_name)

Indexes: idx_rondo_club_work_history_member on (knvb_id)


Raw team membership data from Sportlink (player/staff roles).

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
sportlink_team_idTEXTSportlink team ID
sportlink_person_idTEXTKNVB ID
role_descriptionTEXT”Trainer”, “Keeper”, “Speler”, etc.
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast time membership appeared
created_atTEXTFirst seen timestamp

Unique Constraint: (sportlink_team_id, sportlink_person_id)

Indexes:

  • idx_sportlink_team_members_person on (sportlink_person_id)
  • idx_sportlink_team_members_team on (sportlink_team_id)

Committee records from Sportlink.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
commissie_nameTEXTCommittee name - UNIQUE
sportlink_idTEXTSportlink committee ID - UNIQUE
rondo_club_idINTEGERWordPress commissie post ID
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast time committee appeared
last_synced_atTEXTLast successful sync
last_synced_hashTEXTHash of last synced data
created_atTEXTFirst seen timestamp

Indexes:

  • idx_rondo_club_commissies_hash on (source_hash, last_synced_hash)
  • idx_rondo_club_commissies_name on (commissie_name)

Club-level functions held by members (e.g., “Voorzitter”, “Secretaris”).

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
function_descriptionTEXTFunction name
relation_startTEXTStart date
relation_endTEXTEnd date (NULL = active)
is_activeINTEGER1 = active, 0 = ended
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, function_description)

Indexes: idx_sportlink_member_functions_knvb on (knvb_id)


Committee memberships with roles.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
committee_nameTEXTCommittee name
sportlink_committee_idTEXTSportlink committee ID
role_nameTEXTRole within committee
relation_startTEXTStart date
relation_endTEXTEnd date (NULL = active)
is_activeINTEGER1 = active, 0 = ended
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, committee_name)

Indexes: idx_sportlink_member_committees_knvb on (knvb_id)


Committee membership work history synced to WordPress ACF repeater.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
commissie_nameTEXTCommittee name
role_nameTEXTRole in committee
rondo_club_work_history_idINTEGERWordPress work_history row index
is_backfillINTEGER1 if from historical backfill
source_hashTEXTSHA-256 hash
last_synced_hashTEXTHash of last synced data
last_synced_atTEXTLast successful sync
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, commissie_name, role_name)

Indexes: idx_rondo_club_commissie_work_history_member on (knvb_id)


Free fields from Sportlink /other tab.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID - UNIQUE
freescout_idINTEGERFreeScout customer ID (from Remarks3)
vog_datumTEXTVOG certificate date (from Remarks8)
has_financial_blockINTEGERFinancial transfer block (from MemberHeader)
photo_urlTEXTPhoto URL (from MemberHeader)
photo_dateTEXTPhoto date (from MemberHeader)
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Indexes: idx_sportlink_member_free_fields_knvb on (knvb_id)


Invoice/billing data from Sportlink /financial tab (populated with --with-invoice flag).

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID - UNIQUE
invoice_streetTEXTInvoice street name
invoice_house_numberTEXTHouse number
invoice_house_number_additionTEXTHouse number addition
invoice_postal_codeTEXTPostal code
invoice_cityTEXTCity
invoice_countryTEXTCountry
invoice_address_is_defaultINTEGER1 if default address
invoice_last_nameTEXTInvoice contact last name
invoice_infixTEXTInvoice contact infix
invoice_initialsTEXTInvoice contact initials
invoice_emailTEXTInvoice email
invoice_external_codeTEXTExternal reference code
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Indexes: idx_sportlink_member_invoice_data_knvb on (knvb_id)


Discipline (tucht) cases from Sportlink.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
dossier_idTEXTUnique case ID (e.g., T-12345) - UNIQUE
public_person_idTEXTKNVB ID of person involved
match_dateTEXTMatch date
match_descriptionTEXTMatch details
team_nameTEXTTeam name
charge_codesTEXTKNVB charge code(s)
charge_descriptionTEXTFull charge description
sanction_descriptionTEXTSanction/penalty description
processing_dateTEXTDate case was processed
administrative_feeREALFee amount in euros
is_chargedINTEGERWhether fee was charged
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Indexes: idx_discipline_cases_person on (public_person_id)

Module: lib/discipline-db.js


Tracks field changes detected in Rondo Club for reverse sync.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
field_nameTEXTChanged field name
old_valueTEXTPrevious value
new_valueTEXTNew value
detected_atTEXTWhen change was detected
rondo_club_modified_gmtTEXTWordPress modification timestamp (GMT)
detection_run_idTEXTID of the detection run
synced_atTEXTWhen change was synced back to Sportlink

Indexes:

  • idx_rondo_club_change_detections_knvb on (knvb_id)
  • idx_rondo_club_change_detections_detected on (detected_at)

Audit log of conflicts between Rondo Club and Sportlink data during reverse sync.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
field_nameTEXTConflicting field
sportlink_valueTEXTValue in Sportlink
rondo_club_valueTEXTValue in Rondo Club
sportlink_modifiedTEXTSportlink modification timestamp
rondo_club_modifiedTEXTRondo Club modification timestamp
winning_systemTEXTWhich system’s value was kept
resolution_reasonTEXTWhy that system won
resolved_atTEXTWhen conflict was resolved

Indexes: idx_conflict_resolutions_knvb on (knvb_id)


Singleton table tracking the last reverse sync detection run.

ColumnTypeDescription
idINTEGERPrimary key (always 1)
last_detection_atTEXTTimestamp of last detection run
updated_atTEXTWhen this record was last updated

Constraint: CHECK (id = 1) - Only one row allowed.


Purpose: Tracks member contribution/dues data from Nikki accounting system.

Module: lib/nikki-db.js

Member contribution records per year.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID
yearINTEGERContribution year
nikki_idTEXTNikki system ID
saldoREALOutstanding balance (positive = owes money)
statusTEXTPayment status
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
created_atTEXTFirst seen timestamp

Unique Constraint: (knvb_id, year)

Indexes:

  • idx_nikki_contributions_knvb_id on (knvb_id)
  • idx_nikki_contributions_year on (year)
  • idx_nikki_contributions_saldo on (saldo)

Purpose: Tracks FreeScout customer synchronization and conversation-to-activity sync.

Module: lib/freescout-db.js

FreeScout customer records mapped from Rondo Club members.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
knvb_idTEXTMember KNVB ID - UNIQUE
emailTEXTCustomer email
freescout_idINTEGERFreeScout customer ID
data_jsonTEXTFull customer data as JSON
source_hashTEXTSHA-256 hash
last_seen_atTEXTLast seen timestamp
last_synced_atTEXTLast successful sync
last_synced_hashTEXTHash of last synced data
created_atTEXTFirst seen timestamp

Critical: freescout_id maps KNVB ID to FreeScout customer ID. Without this mapping, sync creates duplicate customers.


Tracks FreeScout conversations synced as activities to Rondo Club, preventing duplicate activity creation.

ColumnTypeDescription
idINTEGERPrimary key (auto-increment)
conversation_idINTEGERFreeScout conversation ID - UNIQUE
freescout_customer_idINTEGERFreeScout customer ID
knvb_idTEXTMember KNVB ID
rondo_club_person_idINTEGERRondo Club person post ID
subjectTEXTConversation subject
statusTEXTConversation status (active, pending, closed)
synced_atTEXTWhen conversation was synced as activity
created_atTEXTWhen record was first tracked

Indexes: idx_freescout_conversations_knvb on (knvb_id)

Module: lib/freescout-db.js


The rondo_club_members.photo_state field implements a state machine for photo synchronization.

StateDescription
no_photoMember has no photo in Sportlink
pending_downloadPhoto exists, needs to be downloaded
downloadedPhoto downloaded to local filesystem
pending_uploadPhoto ready to be uploaded to Rondo Club
syncedPhoto successfully uploaded to Rondo Club
pending_deletePhoto removed from Sportlink, needs deletion
no_photo → pending_download (photo added in Sportlink)
pending_download → downloaded (photo downloaded successfully)
downloaded → pending_upload (ready for upload)
pending_upload → synced (upload successful)
synced → pending_delete (photo removed from Sportlink)
pending_delete → no_photo (deletion successful)
synced → pending_download (photo changed in Sportlink)

Photo changes are detected by comparing photo_date (from MemberHeader API):

  • Added: photo_date changes from NULL to a date
  • Changed: photo_date changes to a different date
  • Removed: photo_date changes to NULL

Primary identifier: knvb_id (KNVB public person ID)

  • Used across all systems
  • Stable over time
  • Links members to teams, committees, contributions

Parent identification: email (no KNVB ID for parents)

  • Linked to children via childKnvbIds array in data_json
-- Laposta: find member by email
SELECT * FROM members WHERE email = '[email protected]';
-- Rondo Club: get WordPress post ID
SELECT rondo_club_id FROM rondo_club_members WHERE knvb_id = 'KNVB123456';
-- Nikki: get contributions
SELECT * FROM nikki_contributions WHERE knvb_id = 'KNVB123456' ORDER BY year DESC;
-- FreeScout: get customer ID
SELECT freescout_id FROM freescout_customers WHERE knvb_id = 'KNVB123456';

Intra-Database Relationships (rondo-sync.sqlite)

Section titled “Intra-Database Relationships (rondo-sync.sqlite)”
-- Member → Teams (via work history)
SELECT team_name FROM rondo_club_work_history WHERE knvb_id = 'KNVB123456';
-- Member → Committees
SELECT committee_name, role_name FROM sportlink_member_committees WHERE knvb_id = 'KNVB123456';
-- Team → Members
SELECT sportlink_person_id, role_description
FROM sportlink_team_members WHERE sportlink_team_id = 'TEAM_ID';
-- Member → Discipline cases
SELECT * FROM discipline_cases WHERE public_person_id = 'KNVB123456';
-- Parent → Children (requires parsing data_json)
SELECT data_json FROM rondo_club_parents WHERE email = '[email protected]';

DatabaseTablesPurpose
laposta-sync.sqlite3Email marketing sync (Laposta)
rondo-sync.sqlite16WordPress sync (members, teams, committees, photos, discipline, reverse sync)
nikki-sync.sqlite1Contribution tracking (Nikki)
freescout-sync.sqlite2Customer sync + conversation tracking (FreeScout)

Total: 22 tables across 4 databases

Common pattern: All main tables use source_hash / last_synced_hash for efficient change detection and idempotent sync operations.