Back to projects

Connecting Booking Systems to Access Control

How I connected a booking system to an access control system so door permissions follow room allocations automatically—without anyone remembering to do it.

The Integration Gap

A university college has two separate systems that need to work together:

  • KX (Kinetic Solutions) - the accommodation system. Staff book students into rooms here. It knows "John Doe is in Room A26 from October to July."
  • Salto Space - the door lock system. Controls which keycards open which doors. It knows "John Doe's card can open Door A26."

The problem isn't just "two systems don't talk". It's ownership. KX is where decisions happen (who gets which room, and when). Salto is where the door actually opens. That gap creates a question nobody wants to answer: who turns a booking into access?

Three departments touch this process: Accommodation creates bookings, Porters deal with keys and lockouts, Student Services handle arrivals and changes. Without automation, everyone assumes it's "someone else's job".

Spider-Man meme showing Student Services, Porters, and Accommodation pointing at each other
The information lives in KX, but the action happens in Salto. Without a link between them, responsibility gets blurry.

Before the integration: Manual work. Someone opens Salto, finds the right doors (bedroom, building entrance, shared facilities), and copies dates from KX. Slow. Repetitive. Error-prone. A single typo becomes a student standing outside their room.

I built an integration that makes this automatic. KX stays the source of truth. Salto gets updated on a schedule. Access follows bookings without anyone remembering to do it.

The Disconnect: Why a JOIN is Impossible

KX Logo

KX System

Accommodation Booking

This system knows:
• Who: John Doe
• Which room: Cripps A 26
• When: 2024-10-14 to 2025-07-03
NO LINK
Salto Space Logo

Salto Space

Door Lock System

Salto showing Cripps A26 door with cryptic GUID 3557A522B4866FCA72B008DCEC71A274
This system knows:
• Door name: Cripps A26
• Door ID: 3557A522...
But no link to KX!

The problem: KX knows "Room 360" and Salto knows "Door A26" - but there's no way to connect them. So porters had to manually look up each room and type it into the door system.

KX knows who lives where and when. Salto controls which keycards open which doors. But they don't share a common identifier. Looking at these tables as an engineer, there is no reliable way to JOIN them.

The Data Reality (BEFORE)

Table: KX Rooms (Source)

Room_ID Room_Name Block_ID Block_Name
360 Cripps A 26 53 Cripps A
361 Cripps A 27 53 Cripps A
420 Cripps B 14 54 Cripps B

Table: Salto Doors (Target) - BEFORE

Door_ID Door_Name ExtLockID (BEFORE)
937 Cripps A26 3557A522B4866FCA72B008DCEC71A274
898 Cripps A27 8F2B3D4E5A6C7B8D9E0F1A2B3C4D5E6F
922 Cripps B14 A1B2C3D4E5F67890ABCDEF1234567890

The Problem: No foreign key relationship exists. String matching ("Cripps A 26" vs "Cripps A26") is fragile and error-prone. GUIDs are meaningless to KX. One typo in manual entry means a student can't get into their own room.

KX allocation interface showing a student booking
The allocation in KX is the starting point for every access decision.

Creating a Common Language

1. Mapping doors to rooms

KX calls a room "Room 360". Salto calls the same door "Cripps A26" with a random internal ID. To connect them, I needed a shared key.

Before: Salto showing Cripps A26 with cryptic GUID (3557A522B4866FCA72B008DCEC71A274). After: Same door renamed to KX_360. KX hierarchy on left showing room 360 = Cripps A 26
Door ID mapping: KX shows room 360 = "Cripps A 26". Salto had a cryptic GUID (3557A522B4866FCA72B008DCEC71A274, top). After renaming to KX_360 (bottom), the systems can talk.

I created a naming convention. Rename every door in Salto to include the KX room ID.

The pattern: KX_ + room_id. Room 360 in KX becomes KX_360 in Salto. Now my code can match them.

How I matched every door

1
Exported all rooms from KX to a CSV file
2
Exported all doors from Salto to another CSV
3
Used an LLM to match names like "Cripps A 26" to "Cripps A26" (same room, different formatting)
4
Generated SQL to update Salto ExtLockID values to the KX_ naming pattern (400+ doors)

The Data Reality (AFTER)

Once I had the mapping, I generated a SQL script to update all the ExtLockID values in Salto. This created a deterministic foreign key relationship.

Table: KX Rooms (Unchanged)
Room_ID Room_Name Block_ID
360 Cripps A 26 53
361 Cripps A 27 53
420 Cripps B 14 54
Table: Salto Doors (Modified) - AFTER
Door_ID Door_Name ExtLockID (AFTER)
937 Cripps A26 KX_360
898 Cripps A27 KX_361
922 Cripps B14 KX_420
... (hundreds more)

The Solution: Now the connection is reliable:

SELECT *
FROM kx_rooms
INNER JOIN salto_doors
    ON CONCAT('KX_', kx_rooms.Room_ID) = salto_doors.ExtLockID

We replaced fragile name-matching with a solid link.

The SQL script to transform Salto:

SQL
1-- Run once to set ExtLockID based on LLM mapping
2 
3UPDATE SALTOSPACE.dbo.tb_Locks SET ExtLockID = 'KX_360' WHERE id_lock = 937;
4UPDATE SALTOSPACE.dbo.tb_Locks SET ExtLockID = 'KX_361' WHERE id_lock = 898;
5UPDATE SALTOSPACE.dbo.tb_Locks SET ExtLockID = 'KX_420' WHERE id_lock = 922;
6-- ... (hundreds more)

Handling What KX Cannot See

Individual room doors are the easy part. But students also need access to shared spaces: building entrance, stairwell, laundry room. KX only knows bedrooms (bookable rooms). Salto knows ALL doors. This gap needed explicit modeling.

KX showing Cripps A (block 53) and Salto showing the staircase renamed from GUID to KXZ_53
Zone mapping: KX groups rooms into "blocks" (Cripps A = block 53). I renamed Salto zones to KXZ_53 using the same pattern. Now when someone is booked into any room in Cripps A, they automatically get access to all Cripps A common areas.

A student in Cripps A room 26 (room ID 360, block ID 53) automatically gets:

  1. Their bedroom door (KX_360)
  2. All common areas in their building (KXZ_53) - entrance, stairs, corridors, laundry

2. Handling shared bathrooms

Some bathrooms are shared between two or three rooms. These don't have their own KX room ID—they're facilities, not bedrooms.

Solution: Use Salto's "Notes" field to list which rooms share that bathroom. If the Notes field says KX_360,KX_361, then anyone in room 360 or 361 gets access.

Floor plan showing individual rooms and shared facilities with Salto door configurations
Shared facilities solution: Individual rooms get KX_ names (Cripps A26 = KX_360). Shared bathrooms keep their GUIDs but use the Notes field to list which rooms they serve: KX_360,KX_361. When the sync runs, anyone in room 360 or 361 gets access to that bathroom.

Source of Truth: Who Wins?

When KX says one thing and Salto says another, what wins? This wasn't just a technical choice—it was a process decision.

We evaluated three strategies and agreed on a path forward with the different departments.

Strategy Logic Pros Cons
1. Preserves Salto (original) Salto dates are sacred. Only add new access. Safe against overwrites. Lockouts. If KX extends dates but Salto doesn't, the student is locked out.
2. Preserves KX (chosen) KX is authoritative. Salto mirrors KX exactly. Single source of truth. Can extend AND revoke. Strict. Removes manual overrides. Requires discipline.
3. Date Extension Use widest range (earliest start, latest end). Zero lockouts. Self-healing. Security. Cannot revoke access early. Requires manual cleanup.

The Result: Preserves KX

  • Single source of truth: Room allocations live in KX. Period.
  • No "shadow state": One place to check: KX.
  • Forces discipline: If someone needs early access, the change happens in KX.

Manual overrides must be reflected in KX or kept outside the sync. This stopped mystery lockouts caused by old manual data.

Architecture

The system follows a strict Extract → Stage → Calculate → Apply pattern. By decoupling the logic into a staging database, the process is:

  • Idempotent: Can run multiple times safely without side effects
  • Auditable: The staging table can be inspected before Salto applies changes
  • Decoupled: Python moves data. SQL contains business logic.
Mermaid
%%{init: {'theme':'dark', 'themeVariables': { 'fontSize':'16px'}}}%%
graph TB
    KX["KX Live
(SQL Server)
Room Allocations"] CSV["CSV Cards
(File System)
Valid Card Holders"] SALTO_IN["Salto System
(SQL Server)
Users, Doors, Zones"] KX -.-> DB_TOOL CSV -.-> DB_TOOL SALTO_IN -.-> DB_TOOL DB_TOOL["db_tool.py
(Python ETL)
Cross-DB Sync Tool"] DB_TOOL ==> STAGING STAGING["MySQL/MariaDB
Staging Database
staging_users_doors_zones"] STAGING ==> RULES RULES["Business Rules
(SQL Views)
Door & Zone assignments"] RULES ==> REPORTS RULES ==> LISTS LISTS ==> SALTO_OUT REPORTS["Validation Reports
(Excel Files)"] LISTS["List Generation
ExtDoorIDList
ExtZoneIDList"] SALTO_OUT["Salto Space
Database Sync
(reads staging table)"] classDef default fill:#334155,stroke:#64748b,stroke-width:2px,color:#e2e8f0

Data flows from three sources, merges in staging, business rules apply via SQL views, and output splits into validation reports (for humans) and access lists (for Salto).

The staging table

The heart of this operation isn't the Python script. It's the database schema. Salto's Database Sync reads from a staging table we populate. Here's what a single student row looks like:

staging_users_doors_zones Row 1 of 523
crsid
'jd456'
FirstName
'John'
LastName
'Doe'
ToBeProcessedBySalto
1 // Ready for import
ExtDoorIDList
{KX_360, 0, 2024-10-01T00:00:00, 2025-06-28T00:00:00}, {KX_361}
^ Room 360 (time-limited) + Shared Bathroom 361 (permanent)
ExtZoneIDList
{KXZ_53, 0, 2024-10-01T00:00:00, 2025-06-28T00:00:00}
^ Cripps Building zone access

Key columns: ExtDoorIDList and ExtZoneIDList contain formatted access grants.

When ToBeProcessedBySalto = 1, Salto reads the row, parses the lists, and grants permissions. Our entire job is generating those strings correctly.

The sync tool

I needed a simple tool to pull data from multiple sources and to run SQL files against each database reliably. db_tool.py does exactly that: it executes a SQL file against a chosen source and writes the results into another database or an export file.

The diagram below shows its two modes: sync (database to database) and export (database to file).

Mermaid
%%{init: {'theme':'dark', 'themeVariables': { 'fontSize':'16px'}}}%%
graph LR
    subgraph SYNC["SYNC MODE (--to)"]
        direction LR
        SQL1["SQL Server"]
        MYSQL1["MySQL"]
        FILE1[".sql File"]
        TOOL1["db_tool.py
(--to)"] OUT1["MySQL"] OUT2["SQL Server"] SQL1 -.-> TOOL1 MYSQL1 -.-> TOOL1 FILE1 -.-> TOOL1 TOOL1 ==> OUT1 TOOL1 ==> OUT2 end subgraph EXPORT["EXPORT MODE (--out)"] direction LR SQL2["SQL Server"] MYSQL2["MySQL"] FILE2[".sql File"] TOOL2["db_tool.py
(--out)"] EXCEL["Excel"] CSV["CSV"] SQL2 -.-> TOOL2 MYSQL2 -.-> TOOL2 FILE2 -.-> TOOL2 TOOL2 ==> EXCEL TOOL2 ==> CSV end classDef default fill:#334155,stroke:#64748b,stroke-width:2px,color:#e2e8f0

Usage is simple: pass a SQL file, pick a source, and pick a destination.

Terminal
1# Sync room allocations from KX Live to local MariaDB staging
2python db_tool.py sync \
3 --truncate \
4 --sql queries/sync-room-allocations-from-kxlive.sql \
5 --from kxlive \
6 --to mariavdt \
7 --table kxroomallocations
8 
9# Export a validation report to Excel
10python db_tool.py export \
11 --table critical_students_losing_access_early \
12 --from mariavdt \
13 --out exports/critical_students_losing_access_early.xlsx

The --truncate flag clears the table first. Clean slate each run, no stale data.

Deciding who wins: A political decision

When KX says one thing and Salto says another, what wins? This wasn't a technical question—it was a political one. We evaluated three strategies before the departments agreed on a path forward.

Strategy Logic Pros Cons (Why we rejected it)
1. Preserves Salto (original) Salto dates are sacred. Only add new access. Safest against accidental overwrites. Lockouts. If KX says "July" but Salto says "June", the student is locked out.
2. Preserves KX (chosen) KX is authoritative. Salto mirrors KX exactly. Single source of truth. Can extend AND revoke. Strict. Removes manual overrides. Requires process discipline.
3. Date Extension Use the widest range (earliest start, latest end). Zero lockouts. Self-healing. Security Risk. Can never revoke access early. Requires manual cleanup.

The Verdict: Preserves KX

After testing all scenarios, Accommodation, Porters, and Student Services chose Strategy 2.

  • Single source of truth: Room allocations live in KX. Period.
  • Eliminates "shadow state": No more "but the porter said..." arguments. One place to check: KX.
  • Forces process discipline: If someone needs early access, the change happens in KX.

Manual overrides must be reflected in KX or kept outside the sync. This took convincing, but it stopped mystery lockouts caused by stale manual data.

Deep Dive: The SQL Behind "Preserves KX"

The business logic lives in SQL views, not Python. Here's the core logic that calculates door access:

1SELECT
2 consolidated_kx.crsid,
3 CASE
4 -- New access: KX dates only
5 WHEN salto_users_locks.ExtDoorID IS NULL THEN
6 CONCAT('{', salto_doors.ExtDoorID, ', 0, ',
7 DATE_FORMAT(consolidated_kx.EarliestArrivalDate, '%Y-%m-%dT%H:%i:%s'), ', ',
8 DATE_FORMAT(consolidated_kx.LatestDepartureDate, '%Y-%m-%dT%H:%i:%s'),
9 '}')
10 
11 -- Existing access: KX overwrites Salto dates
12 WHEN salto_users_locks.UsePeriod = 1
13 AND consolidated_kx.EarliestArrivalDate IS NOT NULL THEN
14 CONCAT('{', salto_doors.ExtDoorID, ', 0, ',
15 DATE_FORMAT(consolidated_kx.EarliestArrivalDate, '%Y-%m-%dT%H:%i:%s'), ', ',
16 DATE_FORMAT(consolidated_kx.LatestDepartureDate, '%Y-%m-%dT%H:%i:%s'),
17 '}')
18 
19 -- Fallback: permanent access (no dates in KX)
20 ELSE CONCAT('{', salto_doors.ExtDoorID, '}')
21 END AS door_access_string
22FROM consolidated_kx
23INNER JOIN salto_doors ON
24 -- Direct room match
25 consolidated_kx.ExtDoorID = salto_doors.ExtDoorID
26 OR
27 -- Shared facility match (bathrooms via Notes field)
28 FIND_IN_SET(consolidated_kx.ExtDoorID, salto_doors.Notes) > 0
29LEFT JOIN salto_users_locks ON
30 salto_users_locks.crsid = consolidated_kx.crsid
31 AND salto_users_locks.ExtDoorID = salto_doors.ExtDoorID

The FIND_IN_SET join is the key to shared bathrooms. If salto_doors.Notes contains KX_360,KX_361, then any student with ExtDoorID = KX_360 or KX_361 gets access to that bathroom.

The daily pipeline

The whole thing runs automatically every hour. Here's the flow:

Pipeline Flow
1┌─────────────────────────────────────────────────────────────────┐
2│ STEP 0: BACKUP │
3│ Snapshot staging_users_doors_zones for audit / rollback │
4└────────────────────────────┬────────────────────────────────────┘
5
6
7┌─────────────────────────────────────────────────────────────────┐
8│ STEP 1: LOAD │
9│ Pull fresh data from three sources into staging database │
10│ - KX room allocations (SQL Server) │
11│ - Salto users, doors, zones (SQL Server) │
12│ - Valid card holders (CSV file) │
13└────────────────────────────┬────────────────────────────────────┘
14
15
16┌─────────────────────────────────────────────────────────────────┐
17│ STEP 2: SNAPSHOT │
18│ Capture current Salto state for audit and comparison │
19└────────────────────────────┬────────────────────────────────────┘
20
21
22┌─────────────────────────────────────────────────────────────────┐
23│ STEP 3: CALCULATE │
24│ Run SQL views to apply business rules │
25│ - Match users to their doors (direct + shared facilities) │
26│ - Calculate zone access (building entrances) │
27│ - Build ExtDoorIDList and ExtZoneIDList strings │
28└────────────────────────────┬────────────────────────────────────┘
29
30
31┌─────────────────────────────────────────────────────────────────┐
32│ STEP 4: UPDATE │
33│ Write calculated access lists back to staging table │
34│ - Format ready for Salto consumption │
35└────────────────────────────┬────────────────────────────────────┘
36
37
38┌─────────────────────────────────────────────────────────────────┐
39│ STEP 5: SALTO SYNC │
40│ Salto import process reads staging table │
41│ - Assigns door/zone permissions to cards │
42│ - Students can now open their doors │
43└────────────────────────────┬────────────────────────────────────┘
44
45
46┌─────────────────────────────────────────────────────────────────┐
47│ STEP 6: VALIDATION │
48│ Generate Excel reports for manual review │
49│ - Who's missing expected access? │
50│ - Any conflicting room allocations? │
51│ - What changed since last run? │
52└─────────────────────────────────────────────────────────────────┘

Step 0: Back up staging for audit

Capture a timestamped copy of the staging table before any changes.

Bash
1BACKUP_TABLE="$(date +%Y-%m-%d-%H.%M.%S)_staging_users_doors_zones"
2mysql --defaults-group-suffix=mariavdt -D salto_sync_backups \
3 -e "CREATE OR REPLACE TABLE \`${BACKUP_TABLE}\` AS SELECT * FROM salto.staging_users_doors_zones"

Step 1: Copy data into the staging database

Pull fresh data from each source into MariaDB. Then the SQL views can calculate access in one place.

Bash
1# KX room allocations
2python db_tool.py sync --truncate \
3 --sql queries/sync-room-allocations-from-kxlive.sql \
4 --from kxlive --to mariavdt --table kxroomallocations &
Bash
1# Salto system data (doors, users, zones)
2python db_tool.py sync --truncate --sql queries/sync-salto-doors.sql --from vsalto --to mariavdt --table salto_doors
3python db_tool.py sync --truncate --sql queries/sync-salto-users.sql --from vsalto --to mariavdt --table salto_users
4python db_tool.py sync --truncate --sql queries/sync-salto-users-locks.sql --from vsalto --to mariavdt --table salto_users_locks
5python db_tool.py sync --truncate --sql queries/sync-salto-users-zones.sql --from vsalto --to mariavdt --table salto_users_zones
6python db_tool.py sync --truncate --sql queries/sync-salto-zone-locks.sql --from vsalto --to mariavdt --table salto_zone_locks
7python db_tool.py sync --truncate --sql queries/sync-salto-zones.sql --from vsalto --to mariavdt --table salto_zones
Bash
1# Load card issue records from CSV
2mysql --defaults-group-suffix=mariavdt -D salto < queries/load-issued-cards-from-csv.sql

Step 2: Snapshot Salto state for audit

Capture the current Salto-mirrored state before updating access lists.

Bash
1mysql --defaults-group-suffix=mariavdt -D salto < queries/capture-salto-sync-audit.sql

Step 3: Run the SQL logic and update the Salto staging table

SQL views calculate each person's ExtDoorIDList and ExtZoneIDList. Then we write the final strings into the table Salto reads.

Bash
1mysql --defaults-group-suffix=mariavdt -D salto < queries/update-staging-with-access-lists.sql

Step 4: Export validation reports

Even though the process is automated, we still export reports for visibility. Missing cards, date mismatches, conflicts, changes.

Bash
1mkdir -p exports
2python db_tool.py export --table access_date_mismatches --from mariavdt --out exports/access_date_mismatches.xlsx
3python db_tool.py export --table critical_students_losing_access_early --from mariavdt --out exports/critical_students_losing_access_early.xlsx
4python db_tool.py export --table kx_allocations_no_cards --from mariavdt --out exports/kx_allocations_no_cards.xlsx
5python db_tool.py export --table room_conflicts_multiple_students --from mariavdt --out exports/room_conflicts_multiple_students.xlsx
6python db_tool.py export --table students_duplicate_access --from mariavdt --out exports/students_duplicate_access.xlsx
7python db_tool.py export --table users_permanent_access --from mariavdt --out exports/users_permanent_access.xlsx
8python db_tool.py export --table users_with_expired_access --from mariavdt --out exports/users_with_expired_access.xlsx
9python db_tool.py export --table view_sync_changes --from mariavdt --out exports/salto_sync_changes.xlsx
10python db_tool.py export --table view_sync_changes_by_days --from mariavdt --out exports/sync_changes_by_days.xlsx
11python db_tool.py export --table users_wrong_room --from mariavdt --out exports/users_wrong_room.xlsx
12wait
13 
14pwsh ./4-export-to-sharepoint.ps1

Flagging what the sync can't fix

The sync works great when data is clean. But it can't fix missing keycards, misconfigured doors, or students with access to wrong rooms. That's what validation reports are for—they flag issues needing human judgment.

Daily validation reports

Example 1: Students Missing Door Access

Students have a room allocation in KX, but their keycard can't open the door. Could be a new booking that hasn't synced yet, or a door mapping problem.

CRSID Student Name Room (KX) Door ID Impact
jd456 John Doe Cripps A 26 KX_360 Can't open bedroom door
sm782 Sarah Mitchell Cripps B 14 KX_420 Can't open bedroom door
al923 Alex Liu Cripps A 27 KX_361 Can't access shared bathroom

Action: Check if door exists in Salto with correct ExtDoorID, verify student has valid card, run sync manually if needed.

Example 2: Students Missing Zone Access

Students might have door access to their room, but no zone access to enter the building. They'll be stuck at the main entrance.

CRSID Student Name Sublocation Zone Name Impact
rw534 Rachel Wilson KXZ_53 Cripps A Building Can't enter building
mb671 Michael Brown KXZ_54 Cripps B Building Can't use elevators or stairwells

Action: Verify zone mapping between KX sublocation IDs and Salto ExtZoneIDs, then re-run sync.

Example 3: Users with Access to Wrong Rooms

Security red flag: Someone has active access to a door that doesn't match their current KX allocation. Usually when a student moves rooms but old access wasn't revoked.

CRSID Student Name Salto Access (Wrong) KX Allocation (Correct) Risk
ep845 Emma Peters KX_360
Cripps A 26
KX_420
Cripps B 14
Access to old room
tc229 Tom Chen KX_150
Main A 15
No current allocation
Left but still has access

Action: Review and let the sync remove automatic KX-managed doors that are not in the proposed list. Manual doors remain untouched and must be handled separately.

What I learned

SQL views for transparency

SQL views made it easy to audit and debug access decisions. When the Bursar asked why a student was locked out, I could run `SELECT *` and show them exactly what the query returned. Business logic in SQL rather than buried in Python meant anyone with database access could verify the logic.

Naming conventions over mapping tables

Renaming every door in Salto to include the KX room ID (KX_360) meant sync logic could directly match rooms to doors. No separate mapping table to maintain.

Making KX the single source of truth

The hardest part wasn't the code. It was convincing porters that "KX is the single source of truth." People hate losing manual overrides. Took months of discussion.

Business Impact

Operational Efficiency

Eliminated manual data entry between KX and Salto. Accommodation team no longer waits for Porters to update access. Access granted automatically, including evenings and weekends when offices are closed.

Risk Reduction

  • Eliminated "phantom access": Departed students no longer keep access after leaving
  • No more shortcuts: Students get correct time-bounded dates from KX instead of permanent access "because it's easier"
  • One source of truth: Only Accommodation updates KX. Porters don't mirror updates in Salto.
  • Fewer late-night emergencies: Reduced lockouts from "someone forgot to update"

Reliability

Successfully handled full annual intake volume. Zero manual intervention required during peak periods. The system runs hourly without supervision.

Cost

$0 in additional software licensing. Built with existing infrastructure (Python, SQL, scheduled tasks).

Floor plan showing room IDs mapped to Salto doors with Notes field for shared bathrooms
The complete mapping: Individual rooms use KX_ naming (Cripps A26 = KX_360), shared bathrooms use Notes field to list which rooms they serve (KX_360,KX_361). This floor plan shows how naming convention connects KX room allocations to Salto door access.

Three Takeaways

1

Define an ID contract

KX_ for rooms. KXZ_ for zones. Both systems speak the same language.

2

Model the doors KX can't see

Bathrooms, staircases, entrances—they need explicit relationships (Notes field, zone mappings) because the booking system doesn't know they exist.

3

Let allocations drive access, daily

The booking is the decision. The door lock is the enforcement. Sync them automatically, and responsibility stops being blurry.