# Sync changes (https://www.cal-sdk.com/docs/guides/sync)

To mirror your users' calendars into Postgres, apply the changes `syncEvents` returns and save its cursor for the next run. The first sync, without a cursor, returns everything the provider syncs: for most providers that's the whole calendar, while Outlook covers a window.

After that you get back only what changed, though some providers have to reread the whole calendar to find it. Check [how each provider syncs](#how-each-provider-syncs) before you choose how often to run it.

```ts
import { SyncCursorExpiredError, syncEvents } from '@cal-sdk/core';

async function sync(calendarId: string) {
  const stored = await db.syncCursors.get(calendarId);

  try {
    const { changes, nextCursor } = await syncEvents({ calendar, calendarId, cursor: stored });
    await applyChanges(changes);
    await db.syncCursors.set(calendarId, nextCursor);
  } catch (error) {
    if (!SyncCursorExpiredError.isInstance(error)) throw error;
    // The provider no longer accepts the cursor. Start over.
    await db.events.clear(calendarId);
    await db.syncCursors.delete(calendarId);
    await sync(calendarId);
  }
}
```

## Apply changes in order [#apply-changes-in-order]

Apply changes in the order they come. The same event can show up more than once in a round (Outlook does this), and the later entry is the newer one, so applying them in order leaves you with the latest version. An `upsert` carries an `event` to insert, or to replace the one with the same `id`, and a `delete` carries the `eventId` to remove:

```ts
import type { SyncChange } from '@cal-sdk/core';

async function applyChanges(changes: SyncChange[]) {
  for (const change of changes) {
    if (change.type === 'upsert') await db.events.put(change.event);
    else await db.events.delete(change.eventId);
  }
}
```

Sync returns what the provider stores, not computed occurrences: single events, series and exceptions. A daily standup with no end has infinitely many occurrences, so there's no sensible way to sync them all. To show a week, expand series yourself, or call `listEvents` for that window.

## Cursors [#cursors]

Store the cursor as a string and never parse it: depending on the provider, it's a provider token or a list of fingerprints. Save it only after the changes are applied, so a crash in between replays them instead of losing them.

Providers can expire a cursor, for example after a long time unused, and then `syncEvents` throws `SyncCursorExpiredError`. Throw away the cursor and your copy, and sync again without one, as the example at the top does.

## Large syncs [#large-syncs]

`syncEvents` follows every page and returns all changes at once. A first sync of years of history can be large, so take it one page at a time with `paginate: false`:

```ts
let pageToken: string | undefined;
let cursor: string | undefined;

do {
  const page = await syncEvents({ calendar, calendarId, cursor: stored, paginate: false, pageToken });
  await applyChanges(page.changes);
  pageToken = page.nextPageToken;
  cursor = page.nextCursor;
} while (pageToken);

await db.syncCursors.set(calendarId, cursor);
```

Pass the same `calendarId` and `cursor` on every page of a round. The last page has no `nextPageToken` and carries `nextCursor`.

## How each provider syncs [#how-each-provider-syncs]

| Provider          | How                                                               |
| ----------------- | ----------------------------------------------------------------- |
| Google            | Google sync tokens                                                |
| Outlook           | Graph delta queries over a fixed window, by default a year back and two years ahead |
| CalDAV            | The server's sync tokens (RFC 6578)                               |
| Cal.com           | Bookings by the time they last changed                            |
| Calendly          | Rereads every meeting and compares it with the last sync. The cursor grows with the number of events |
| Zoom              | Rereads every meeting and compares it with the last sync. The cursor grows with the number of events |
| ICS               | Downloads the feed unless the server answers `304 Not Modified`, then compares it with the last sync. The cursor grows with the number of events |
| Mock              | A change log in memory                                            |

Outlook syncs a window because Graph's delta query needs a date range. Pass `providerOptions.microsoft.timeMin` and `timeMax` on the first sync to choose a different one; the window is kept in the cursor after that.

No provider pushes changes yet, so run sync on a schedule, or when the user opens your app.
