How I Solved Real-Time Reminder Synchronization in a Tauri Desktop App
A deep dive into managing task reminders and notifications across Rust and React, and why testing edge cases matters when building desktop applications.

Introduction
Building desktop apps is fun—until your reminders start firing at the wrong time, or worse, not firing at all. When I was building ZenTrack (a Pomodoro and task management app with Rust and Tauri), I discovered that syncing reminders between the Rust backend and React frontend is way trickier than I expected. This isn't just about showing a popup; it's about keeping data consistent across two different systems, handling edge cases, and making sure your app doesn't become "the notification app that cried wolf." Let me walk you through what went wrong, how I fixed it, and why this matters to anyone building desktop apps.
The Problem: Reminders That Aren't Reliable
When I first built ZenTrack's reminder system, it seemed straightforward: store a task, calculate when to remind the user, show a notification. Simple.
But then I started testing:
- Scenario 1: User edits a task's due date—does the reminder update? Not always. The old reminder was still stuck in the database.
- Scenario 2: User marks a task as done, then accidentally reopens it—now there are duplicate reminders for the same task.
- Scenario 3: Reminders fire even after tasks are deleted because the database cleanup wasn't happening properly.
The root cause? I wasn't properly recalculating reminders when tasks changed. Every time a task was updated, the old reminder logic still existed, but I wasn't telling it to update.
Understanding the Architecture
Before fixing the problem, I needed to understand how the system worked:
The Flow:
- React frontend → sends task update to Rust backend
- Rust backend → updates SQLite database
- Rust backend → should recalculate reminders
- Rust backend → sends updated data back to React
- React → updates UI and re-renders
The issue was in step 3. When a task changed, I wasn't properly handling the reminder recalculation.
The Solution: Smart Reminder Recalculation
Here's the key code that fixed it. In the Rust backend, I added a function to recalculate reminders whenever a task changed:
pub fn recalculate_reminders_for_task(
conn: &Connection,
task_id: i64,
previous_due_date: Option<&str>,
new_due_date: Option<&str>,
) -> Result<(), String> {
// Step 1: Delete old reminders if due date changed
if previous_due_date != new_due_date {
conn.execute(
"DELETE FROM reminders WHERE task_id = ?1 AND triggered = 0",
params![task_id],
).map_err(|e| e.to_string())?;
}
// Step 2: Create new reminders if task still has a due date
if let Some(due_date) = new_due_date {
let remind_at = calculate_reminder_time(due_date)?;
create_reminder(conn, task_id, remind_at)?;
}
Ok(())
}What this does:
- Compares old vs. new due date: If the user changes when the task is due, we know reminders need updating.
- Deletes old untriggered reminders: We only delete reminders that haven't fired yet, preserving history.
- Creates new reminders: If the task still has a due date, we calculate when to remind and store it.
The Tricky Part: Edge Cases
The code above works, but I also had to handle scenarios like:
What if a user deletes a task? I added a foreign key constraint in SQLite:
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL,
remind_at DATETIME NOT NULL,
triggered BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE
)The ON DELETE CASCADE means: if a task is deleted, automatically delete its reminders too. No orphaned reminders.
What if the same reminder time is set twice? I added a unique constraint:
CREATE UNIQUE INDEX IF NOT EXISTS idx_reminders_unique
ON reminders(task_id, remind_at)This prevents duplicate reminders from being created.
How It Works in the Update Flow
When the React app calls update_task() in Rust, here's what happens:
#[tauri::command]
fn update_task(
state: State<'_, DatabaseConnection>,
task: Task
) -> Result<Vec<Task>, String> {
let conn = state.0.lock().map_err(|e| e.to_string())?;
// Get the old due date BEFORE updating
let previous_due_date: Option<String> = conn
.query_row(
"SELECT due_date FROM tasks WHERE id = ?1",
params![task.id as i64],
|row| row.get(0),
)
.optional()
.map_err(|e| e.to_string())?;
// Update the task in the database
conn.execute(
"UPDATE tasks SET title = ?1, due_date = ?3, priority = ?5 WHERE id = ?7",
// ... params ...
).map_err(|e| e.to_string())?;
// Recalculate reminders with old and new dates
reminders::recalculate_reminders_for_task(
&conn,
task.id as i64,
previous_due_date.as_deref(),
task.due_date.as_deref(),
)?;
// Return updated tasks to React
load_tasks(&conn).map_err(|e| e.to_string())
}Key point: We fetch the old due date before updating, then use both the old and new dates to decide what to do with reminders.
Why This Matters
If you're building a desktop app (or any app with state synchronization), this teaches you:
- Data consistency is critical: When you have data in multiple places (database + memory + UI), changes need to propagate correctly.
- Test edge cases early: The obvious happy path works fine. It's the deletions, re-edits, and conflicting updates that break things.
- Use database constraints: Unique indexes and foreign keys prevent bugs before they happen.
- Communicate between layers: Backend and frontend need to agree on what data looks like after changes.
Conclusion
Building ZenTrack taught me that small details in state management make the difference between a reliable app and a frustrating one. The reminder synchronization system went from broken and unpredictable to solid and trustworthy by properly handling task updates, edge cases, and database constraints. If you're building desktop apps with Tauri, Electron, or any framework, pay attention to how data flows between your backend and frontend—especially when updates happen. Test the weird scenarios. Use your database's tools (constraints, indexes) to enforce correctness. Your future self (and your users) will thank you.
Links
- GitHub: github.com/Priyans00/ZenTrack
- Learn More About Tauri: tauri.app
- SQLite Constraints Guide: sqlite.org/foreignkeys.html