DailyRoundup

Sync Internals

This document covers the internal mechanics of bidirectional sync between Trello and Apple Reminders: which fields are synced, how creates and updates are processed, how the Priority custom field works, how conflicts are detected and resolved, and how echo-loops are prevented.

Synced Fields

Fields fall into three categories based on how far they propagate:

Bidirectional fields (Trello ↔ Reminders)

These fields sync in both directions. Changes on either side are detected and pushed to the other.

Trello field Reminders field Notes
Card name Title Truncated to 16,384 chars on both sides
Card description (desc) Notes Truncated to 16,384 chars on both sides
Due date (due) Due date components ISO 8601 ↔ DateComponents conversion
Start date (start) Start date components ISO 8601 ↔ DateComponents conversion
Completion (dueComplete) isCompleted Boolean; uses dedicated complete/uncomplete change types
Priority custom field priority (EKReminderPriority) Mapped via a managed “Priority” dropdown custom field; see Custom Fields
Card location Location-based alarm Trello address/coordinates ↔ EKStructuredLocation geofence alarm; server geocodes addresses when coordinates are missing

Trello → Reminders only (display-only in the app)

These fields are stored on the server and displayed in the DailyRoundup app but are not written to Reminders (EventKit has no equivalent).

Trello field Storage Display
Labels trello_card_labels table Colored label pills in the task detail view
Attachments sync_attachments table 📎 name: url appended to reminder notes; attachment list in the app
Checklists sync_checklists / sync_checklist_items tables Checklist items with completion status in the task detail view
Custom fields (non-priority) trello_card_custom_field_values table Name/value pairs in the task detail view
Card position (pos) sync_tasks.position column Used for ordering; Reminders uses its own list order

Reminders → Trello only

Reminders field Trello field Notes
Tag names Labels Reminders tags are mapped to Trello labels by name; new labels are created if no match exists

Create vs Update Processing

Card creation and updates follow different paths because Trello’s webhook payloads differ significantly between the two.

Trello → Reminders (inbound)

Create (createCard webhook): Trello’s createCard webhook payload only includes minimal card fields (id, name, idList, pos). It omits desc, due, start, dueComplete, and custom field items. To ensure all fields are captured, the webhook handler fetches the full card from the Trello API (GET /cards/{id}?fields=all&customFieldItems=true) before recording the pending change. The priority custom field is resolved from the card’s customFieldItems using the stored option ID mappings.

The server creates a sync_task record and inserts a pending_change with change_type=create. The iOS client pulls this change, calls EventKitStore.syncCreate() to create the EKReminder, and acknowledges the change.

Update (updateCard webhook): Trello’s updateCard webhook includes only the changed fields in data.old (the previous values). The webhook handler extracts the new values from data.card for each field present in data.old. No API fetch is needed because the webhook payload contains the current values for all changed fields.

Reminders → Trello (outbound)

Create (new reminder detected): During the push phase of a sync cycle, SyncManager compares the current Reminders items against known SyncTask records. Unrecognized items (no matching remindersItemId) are emitted as create changes with all current field values. The server calls TrelloClient.create_card() with title, description, due date, position, and card role, then creates the sync_task record.

Update (field hash mismatch): Each SyncTask stores a hash of every synced field’s last-known value. On each sync cycle, SyncManager recomputes hashes for the current reminder and compares them against the stored hashes. Only changed fields are included in the update change payload, minimizing unnecessary Trello API calls. After the server applies the update, the client refreshes the stored hashes.

Custom Fields (Priority)

Priority is the only custom field that syncs bidirectionally. It is implemented as a managed Trello dropdown (list-type) custom field named “Priority” with three options: Low, Medium, and High. These map to EKReminderPriority values:

Trello option EKReminderPriority raw value Constant
(none selected) 0 .none
High 1 .high
Medium 5 .medium
Low 9 .low

Setup

When the first Reminders-side priority change is pushed for a list, the server calls ensure_priority_custom_field():

  1. Checks if a “Priority” dropdown custom field already exists on the Trello board.
  2. If not, creates one with Low (green), Medium (yellow), and High (red) options.
  3. Stores the custom field ID and option ID mappings in the sync_custom_fields table.

If the board is on a free Trello plan (custom fields require Standard or higher), the server logs a warning and silently skips priority sync for that list.

Sync flow

All other custom fields (text, number, date, checkbox, other dropdowns) are read-only: they are stored in trello_card_custom_field_values and displayed in the app but not synced to Reminders.

Conflict Detection and Resolution

Conflicts occur when both Trello and Reminders modify the same field on the same task before a sync cycle runs. Only a subset of fields participate in conflict detection — last-write-wins fields are excluded.

Conflict-eligible fields

title, description, due_date, start_date

Last-write-wins fields (no conflict detection)

position, is_completed, priority

Detection

When the iOS client pushes a Reminders-side update change to the server (POST /dailyroundup/sync), the server checks for unacknowledged Trello-originated pending changes on the same task that touch any of the same conflict-eligible fields:

  1. Query pending_changes for the task where change_origin=trello.
  2. Extract the changed field names from each pending change’s changed_fields.
  3. Intersect with the incoming Reminders change’s fields.
  4. Normalize values (empty string ↔ None, date format differences) to avoid false positives.
  5. If any field has genuinely different values on both sides, a conflict record is created for each conflicting field.

The conflicting Trello pending changes are marked as conflicted so they are not re-applied or re-detected. The Reminders-side change is not applied — both sides remain unchanged until the user resolves the conflict.

Resolution options

Conflicts are resolved via POST /dailyroundup/conflicts/{id}/resolve with one of three strategies:

Resolution Effect
keep_trello The Trello value is written to the sync_task and a pending change is created to update the Reminders item on the next sync cycle. No Trello API call is needed (the card already has the correct value).
keep_reminders The Reminders value is pushed to the Trello card via the API, and the sync_task is updated to match. An echo-loop fingerprint is recorded to suppress the resulting updateCard webhook.
keep_both The original task keeps the Reminders value. A duplicate Trello card is created with the Trello value and “ [conflict]” appended to its title. A create pending change is emitted so the duplicate appears in Reminders as a separate item.

Boolean fields (is_completed, is_archived) only support keep_trello and keep_reminderskeep_both is rejected with a 400 error.

User interface

Unresolved conflicts are surfaced in three ways:

  1. In-app banner: The sync status bar shows a conflict count badge. Tapping it opens the conflict resolution view where each conflict shows the Trello value, Reminders value, and last-synced value side by side.
  2. Push notification: When the server detects a new conflict, it sends a visible APNs notification to all registered devices for the account. On macOS, the notification includes action buttons for quick resolution.
  3. macOS notification actions: The macOS app delegate handles UNNotificationAction responses, allowing users to resolve conflicts directly from the notification without opening the app.

Echo-Loop Prevention

Because every change the server makes to Trello triggers a webhook back to the server, and every change the app makes to Reminders is detected on the next sync cycle, echo-loop prevention is critical. Without it, a single change would bounce back and forth indefinitely. Four independent mechanisms work together:

1. Server-side fingerprinting (echo_loop.py)

When the server writes to the Trello API (e.g., creates or updates a card), it records a fingerprint in the echo_fingerprints SQLite table:

echo_loop.record("createCard", card_id)   → key = "createCard:<card_id>", TTL = 30s
echo_loop.record("updateCard", card_id)   → key = "updateCard:<card_id>", TTL = 30s

When a webhook arrives, the handler calls echo_loop.check_and_consume(action_type, card_id) before recording any pending change. If a matching fingerprint exists:

Fingerprints are stored in SQLite (not in-memory) so they are shared across all gunicorn worker processes. Expired entries are pruned opportunistically on each insert/check.

Fail-open design: If the database is temporarily unavailable, record() logs a warning but does not raise, and check_and_consume() returns False. This means the webhook is processed rather than suppressed — a safer default than silently dropping legitimate changes.

2. Server-side task ownership check

For createCard webhooks specifically, the handler checks whether the newly created card already has a sync_task with a reminders_item_id. If so, the card was created by the server in response to a Reminders-side change (via _apply_create), and the webhook is an echo. This check catches the race condition where the webhook arrives at a different gunicorn worker before echo_loop.record() executes.

3. Client-side recency window (SyncManager)

The iOS/macOS app maintains an in-memory dictionary mapping remindersItemId → timestamp of the last Trello-originated apply. When detecting Reminders-side changes during the push phase:

private var recentlyApplied: [String: Date] = [:]
private let echoLoopWindow: TimeInterval = 5  // seconds

If a reminder was modified within the last 5 seconds of a Trello-originated apply (create, update, complete, delete), the change is suppressed and not pushed to the server. This prevents the app’s own EventKit writes from being detected as user changes and echoed back.

4. Webhook deduplication (seen_action_ids)

Trello may deliver the same webhook event multiple times (e.g., from board-level and list-level webhook registrations, or network retries). The is_duplicate_action() function records each Trello action.id in the seen_action_ids table with a 5-minute TTL. Subsequent deliveries of the same action are silently dropped. The check uses INSERT OR IGNORE on a UNIQUE column for atomic first-writer-wins semantics.