mcpField
G
google-calendar-sync

google-calendar-sync

github.com/timerise-aiDevelopment03 installsUpdated 3d ago
Repository

About google-calendar-sync

google-calendar-sync is a skill for implementing or auditing Google Calendar synchronization in booking systems. It keeps inbound busy-time imports separate from outbound booking events, with guidance on OAuth, storage, webhooks, channels, and testing.

Key features

  1. 01Imports busy blocks through full-window resyncs
  2. 02Writes booking events with deterministic IDs
  3. 03Handles OAuth connections and encrypted tokens
  4. 04Renews watch channels and schedules resyncs
  5. 05Provides Firestore and Supabase store options
  6. 06Includes testing and operations guidance

Use cases

  1. 01Blocking host availability from selected calendars
  2. 02Adding confirmed bookings to a host calendar
  3. 03Auditing duplicate, stale, or stopped syncs
  4. 04Migrating sync storage between Firestore and Supabase

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/timerise-ai/google-calendar-sync --skill google-calendar-sync

After installing, type/google-calendar-syncin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

CHANGELOG.md
README.md
CLAUDE.md
LICENSE
.gitattributes
.github/workflows/
agent-eval.yml
evals/
prompts.md
references/
booking-events.md
adaptation.md
busy-sync.md
channels-and-settings.md
data-model.md
http-routes.md
operations.md
test-suite.md
google-api.md
oauth-and-tokens.md
firestore-backend.md
supabase-backend.md
provenance.md
testing.md

Details

From the project README

Google Calendar Sync

Two directions, one rule each. Inbound: Google is the truth about a host's other commitments, so busy blocks are a disposable mirror of a time window, rebuilt by a full resync and never edited by hand. Outbound: the booking store is the truth about bookings, so an event written to Google is addressed by an id derived from the booking, and is never read back in as busy time. Keeping the two directions apart is what the templates and their tests hold.

When to use

  • Hosts connect Google Calendar and pick calendars that block their availability.
  • Bookings appear in the host's Google Calendar, optionally with a Meet link, and vanish on cancel.
  • Auditing an existing Google sync: duplicates, ghosts, sync that stopped, reconnect loops.
  • Moving that sync between Firestore and Supabase/Postgres.

When NOT to use

  • Generating bookable slots, subtracting busy time: time-slot-generator. This skill produces the busy blocks it consumes.
  • Outlook or Microsoft 365: microsoft-calendar-sync, the same architecture and record shapes on Microsoft Graph.
  • Showing a calendar UI or sending invitations: the host's own e-mail; this writes events without Google's invitation e-mails.
  • Read-only availability without per-user OAuth: Google's free/busy query with a service account and domain-wide delegation is a different design.

Architecture

          connect (PKCE, single-use state) -> callback -> encrypted tokens -> calendar rows
                                                                                  |
 inbound   Google -- X-Goog-* -> /notifications -> syncBusy (lease) -> list all pages -> toBusyBlock -> diffBusy -> busy blocks -> availability
              ^                                         ^
              +-- watch channel <-- cron: renew new-before-old, resync every busy calendar
 outbound  booking confirmed -> pushBookingEvent -> insert id=bk<sha256> (409: update) -> Meet link back
           booking cancelled -> removeBookingEvent -> delete by id (404/410 = done)
Adaptation contract

adaptation.md has the full contract, the host prerequisites and the order of work. The rename table is in data-model.md.

SeamThis skill shipsThe host supplies
Authauthenticate(request): userId or nullIts session lookup
DataCalendarStore on Firestore and SupabaseIts project
BookingsBookingSource, pushBookingEvent, removeBookingEventCalls from its booking status flow
AvailabilityBusy blocks per user, listHostBusyIts slot engine (time-slot-generator)
HTTPWeb Request and Response handlersNext.js route handlers, or the Express adapter
Background workrenewChannels every 12 h, optional deferIts scheduler, after()

Critical facts

  1. Busy sync is a full window resync, not an event stream. A notification says only "something changed". List every page of [now, now + horizon) and diff. A partial list must abort, never delete.
  2. Ids are deterministic everywhere. Calendar rows, busy blocks and booking events are hashes of what they represent. Concurrent notifications, retries and double triggers then converge instead of duplicating.
  3. Your own events come back. A booking written to a calendar that also has busy sync is re-imported unless it carries the private bookingId marker and is skipped. With seat-based services, that blocks the rest of a group session after the first booking.
  4. Google only tells you what changed while the channel is alive. Channels expire, notifications are not reliably retried, and the horizon moves. The cron renews channels and resyncs every busy calendar; without it, busy time stops updating and nothing reports it.
  5. invalid_grant is a state, not an error. Revoked access, six months unused, or seven days in "Testing" mode. Mark the connection reauth_required, surface it, and let reconnecting the same account keep every setting.
  6. All-day events span start.date to the exclusive end.date, at local midnights of the calendar's zone. Timed events carry offsets. Send outbound instants with their Z; stripping it shifts events.
  7. Free and declined events do not block. Google's own free/busy ignores them. Holiday calendars are the exception: they are only enabled in order to block.

Hard rules

1. Never take the user id from the OAuth callback URL. The state is a key to a server-side, single-use, ten-minute record. Otherwise anyone attaches their calendar to someone else's account.

2. Never trust a notification before its channel token verifies against the channel id it arrived on. The endpoint is public; the token is the only credential.

3. Never stop the old channel before the new one exists. If the new watch fails, the calendar has no channel, nothing renews it, and its busy time stops updating while the UI says "on".

4. Never store tokens in clear text. A refresh token is long-lived read and write access to a calendar.

5. Never let a Google failure fail a booking. Outbound writes log and return; the booking stands.

Quick start

  1. Read the host and fill the seam contract: adaptation.md.
  2. Create the Google Cloud OAuth client and consent screen: oauth-and-tokens.md.
  3. Copy the types and decide the rename: data-model.md.
  4. Copy the Google client: google-api.md.
  5. Copy inbound sync, busy-sync.md; outbound, booking-events.md; lifecycle, channels-and-settings.md.
  6. Pick a store: firestore-backend.md or supabase-backend.md.
  7. Mount the routes and the cron: http-routes.md.
  8. Copy the suite and run it under two TZ values: testing.md, test-suite.md.
  9. Give operators the health view: operations.md.
  10. Fixing an existing integration? The order is in provenance.md.

Reference directory

ScenarioTrigger keywordsReference
Seams and order of workadapt, host probe, seam contract, non-negotiablesadaptation.md
Entities, store contract, renameLinkedCalendar, BusyBlock, WatchChannel, CalendarStore, mutateCalendar, version, compare-and-swapdata-model.md
Connecting an accountOAuth, PKCE, state, consent screen, scopes, refresh_token, invalid_grant, reauth, revoke, AES-256-GCM, Testing modeoauth-and-tokens.md
Talking to Googlefetch, calendarList, events.list, pageToken, events.watch, channels.stop, 409 duplicate, 410, rate limitgoogle-api.md
Busy time intoBusyBlock, transparency, declined, all-day, end.date, DST, diffBusy, lease, resyncRequested, horizonbusy-sync.md
Bookings outevent id, base32hex, extendedProperties private, Meet, conferenceData, hangoutLink, sendUpdates, backfillbooking-events.md
Channels and togglesstartChannel, renewChannels, make-before-break, orphan channel, holiday calendar, syncBusy, syncBookings, planTransitionschannels-and-settings.md
Routes and webhookX-Goog-Channel-ID, X-Goog-Resource-State, sync, exists, 204, callback redirect, cron secret, Next.js, Expresshttp-routes.md
Firestorefirebase-admin, runTransaction, batch 500, calendarBusyBlocks, composite index, security rulesfirestore-backend.md
Supabase / Postgresschema.sql, RLS, service role, optimistic concurrency, 23505, PostgREST 1000 rowssupabase-backend.md
Proving itnode:test, memory store, fake Google, TZ=, mutation checktesting.md, test-suite.md
Running ithealth, stale, channel-expired, env vars, quotas, dashboards, runbook, ngrok, localhostoperations.md
Why it is built this wayaudit, defect, kept deliberately, designed here, fix orderprovenance.md

Part of the Timerise Skills index, which lists the sibling skills.

Comments

Markdown supported