Skip to main content

Overview

Chronos Calendar provides seamless two-way synchronization with Google Calendar, ensuring your events are always up-to-date across all devices. The sync system uses Google’s incremental sync tokens and push notifications for efficient, real-time updates.

How Sync Works

When you first connect a Google Calendar:
  1. Full Sync: Chronos fetches all events from your Google Calendar
  2. Encryption: Event data is encrypted with your user key
  3. Storage: Events are stored in both Supabase (server) and IndexedDB (local)
  4. Sync Token: A sync token is saved to enable incremental updates
The initial sync may take longer for calendars with many events. Progress is displayed in real-time.

Syncing Your Calendar

Manual Sync

Click the Sync button in the calendar header to manually trigger a sync:
  1. Connection: Opens an SSE (Server-Sent Events) stream to the backend
  2. Progress: Displays real-time progress for each calendar
  3. Event Processing: Events are fetched, encrypted, and stored
  4. Completion: Shows total events synced and last sync time
The sync endpoint streams events as they’re fetched:
# From calendar.py:148-223
@router.get("/sync")
async def sync_calendars(
    current_user: CurrentUser,
    supabase: SupabaseClientDep,
    http: HttpClient,
    calendar_ids: str = Query(...),
):
    # SSE stream with real-time progress updates
    async def event_generator():
        events_queue: asyncio.Queue = asyncio.Queue()
        # ... processes events and yields SSE messages

Automatic Sync

Chronos automatically syncs your calendars:
  • Every 10 minutes while the app is open
  • On startup when you launch the app
  • Via webhooks when Google Calendar sends push notifications
  • Smart polling checks for server-side updates every minute
// From useCalendarSync.ts:15-17
const POLL_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
const MAX_FOREGROUND_SYNC_ATTEMPTS = 5;
const FOREGROUND_SYNC_RETRY_DELAY_MS = 1000;

Sync Status Indicators

The calendar header displays sync status:

Syncing

Progress bar shows events being fetched and calendars completed

Up to Date

Displays last sync timestamp (e.g., “Synced 2 minutes ago”)

Error

Shows error message with retry option

Needs Reauth

Google account requires re-authentication

Handling Sync Errors

Token Expiration

If a sync token expires (error 410), Chronos automatically:
  1. Clears the expired token
  2. Performs a fresh full sync
  3. Saves the new sync token
# From sync.py:130-136
if e.status_code == 410 and not is_retry:
    logger.info("Sync token expired for calendar %s, clearing and retrying full sync", calendar_id)
    await asyncio.to_thread(clear_calendar_sync_state, supabase, calendar_id)
    sync_token = None
    page_token = None
    is_retry = True
    continue

Rate Limiting

Syncs are rate-limited to once every 5 seconds per user to prevent abuse and quota exhaustion.

Connection Issues

If sync fails due to network issues:
  • Retryable errors (500, 429, quota): Automatically retried with exponential backoff
  • Non-retryable errors (401, 403): Displayed to user with action required
  • Timeout: Sync cancelled after 5 minutes (300 seconds)

Multi-Calendar Sync

You can sync up to 20 calendars simultaneously:
  • Concurrent Fetching: Up to 5 calendars fetched in parallel
  • Independent Progress: Each calendar’s progress tracked separately
  • Partial Success: If one calendar fails, others continue syncing
// From useCalendarSync.ts:136-177
const params = new URLSearchParams({ calendar_ids: ids.join(",") });
const url = `${getApiUrl()}/calendar/sync?${params.toString()}`;

const eventSource = new EventSource(url, { withCredentials: true });

eventSource.addEventListener("events", async (e) => {
  const payload = JSON.parse(e.data);
  await processEvents(payload);
  eventsLoaded += payload.events.length;
});

Data Privacy

All event data is encrypted at rest using user-specific encryption keys. Even if the database is compromised, your calendar data remains secure.
Event encryption happens before storage:
# From sync.py:30-38
async def add_events(supabase: Client, events: list[dict], user_id: str, calendar_id: str) -> None:
    if not events:
        return
    try:
        encrypted = await asyncio.to_thread(encrypt_events, events, user_id)
        await asyncio.to_thread(upsert_events, supabase, encrypted)
    except Exception:
        logger.exception("Failed to encrypt/upsert events for calendar %s", calendar_id)
        raise

Troubleshooting

  1. Check that your Google account doesn’t show “Needs Reauth”
  2. Try clicking the sync button manually
  3. Check your internet connection
  4. Look for error messages in the calendar header
  • Manual sync: Click the sync button
  • Webhooks may not be working: Check server logs for webhook errors
  • Browser may be offline: Check network connectivity
  • 401 Unauthorized: Re-authenticate your Google account
  • 403 Forbidden: Check Google Calendar API quota limits
  • 429 Rate Limited: Wait a few minutes before trying again
  • 500 Server Error: Google Calendar may be temporarily unavailable

Technical Details

Sync Flow Architecture

  1. Client Request: Frontend initiates sync via SSE endpoint
  2. Token Management: Backend refreshes OAuth tokens if needed
  3. Parallel Fetching: Up to 5 calendars fetched concurrently
  4. Event Streaming: Events streamed to client as they’re fetched
  5. Local Storage: Events stored in IndexedDB for offline access
  6. Webhook Registration: Push notification channels registered/renewed

OAuth Token Refresh

# From gcal.py:62-78
async def get_valid_access_token(http: httpx.AsyncClient, supabase: Client, user_id: str, google_account_id: str) -> str:
    tokens = get_decrypted_tokens(supabase, user_id, google_account_id)
    expires_at = parse_expires_at(tokens["expires_at"])

    if not token_needs_refresh(expires_at):
        return tokens["access_token"]

    lock = await get_refresh_lock(google_account_id)
    async with lock:
        # ... refresh token logic
Tokens are automatically refreshed when they’re about to expire, ensuring uninterrupted sync operations.

Build docs developers (and LLMs) love