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
- 01Imports busy blocks through full-window resyncs
- 02Writes booking events with deterministic IDs
- 03Handles OAuth connections and encrypted tokens
- 04Renews watch channels and schedules resyncs
- 05Provides Firestore and Supabase store options
- 06Includes testing and operations guidance
Use cases
- 01Blocking host availability from selected calendars
- 02Adding confirmed bookings to a host calendar
- 03Auditing duplicate, stale, or stopped syncs
- 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-syncAfter installing, type/google-calendar-syncin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
CHANGELOG.mdREADME.mdCLAUDE.mdLICENSE.gitattributes.github/workflows/agent-eval.ymlevals/prompts.mdreferences/booking-events.mdadaptation.mdbusy-sync.mdchannels-and-settings.mddata-model.mdhttp-routes.mdoperations.mdtest-suite.mdgoogle-api.mdoauth-and-tokens.mdfirestore-backend.mdsupabase-backend.mdprovenance.mdtesting.mdDetails
From the project READMEGoogle 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.
| Seam | This skill ships | The host supplies |
|---|---|---|
| Auth | authenticate(request): userId or null | Its session lookup |
| Data | CalendarStore on Firestore and Supabase | Its project |
| Bookings | BookingSource, pushBookingEvent, removeBookingEvent | Calls from its booking status flow |
| Availability | Busy blocks per user, listHostBusy | Its slot engine (time-slot-generator) |
| HTTP | Web Request and Response handlers | Next.js route handlers, or the Express adapter |
| Background work | renewChannels every 12 h, optional defer | Its scheduler, after() |
Critical facts
- 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. - 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.
- Your own events come back. A booking written to a calendar that also has busy sync is re-imported
unless it carries the private
bookingIdmarker and is skipped. With seat-based services, that blocks the rest of a group session after the first booking. - 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.
invalid_grantis a state, not an error. Revoked access, six months unused, or seven days in "Testing" mode. Mark the connectionreauth_required, surface it, and let reconnecting the same account keep every setting.- All-day events span
start.dateto the exclusiveend.date, at local midnights of the calendar's zone. Timed events carry offsets. Send outbound instants with theirZ; stripping it shifts events. - 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
- Read the host and fill the seam contract: adaptation.md.
- Create the Google Cloud OAuth client and consent screen: oauth-and-tokens.md.
- Copy the types and decide the rename: data-model.md.
- Copy the Google client: google-api.md.
- Copy inbound sync, busy-sync.md; outbound, booking-events.md; lifecycle, channels-and-settings.md.
- Pick a store: firestore-backend.md or supabase-backend.md.
- Mount the routes and the cron: http-routes.md.
- Copy the suite and run it under two
TZvalues: testing.md, test-suite.md. - Give operators the health view: operations.md.
- Fixing an existing integration? The order is in provenance.md.
Reference directory
| Scenario | Trigger keywords | Reference |
|---|---|---|
| Seams and order of work | adapt, host probe, seam contract, non-negotiables | adaptation.md |
| Entities, store contract, rename | LinkedCalendar, BusyBlock, WatchChannel, CalendarStore, mutateCalendar, version, compare-and-swap | data-model.md |
| Connecting an account | OAuth, PKCE, state, consent screen, scopes, refresh_token, invalid_grant, reauth, revoke, AES-256-GCM, Testing mode | oauth-and-tokens.md |
| Talking to Google | fetch, calendarList, events.list, pageToken, events.watch, channels.stop, 409 duplicate, 410, rate limit | google-api.md |
| Busy time in | toBusyBlock, transparency, declined, all-day, end.date, DST, diffBusy, lease, resyncRequested, horizon | busy-sync.md |
| Bookings out | event id, base32hex, extendedProperties private, Meet, conferenceData, hangoutLink, sendUpdates, backfill | booking-events.md |
| Channels and toggles | startChannel, renewChannels, make-before-break, orphan channel, holiday calendar, syncBusy, syncBookings, planTransitions | channels-and-settings.md |
| Routes and webhook | X-Goog-Channel-ID, X-Goog-Resource-State, sync, exists, 204, callback redirect, cron secret, Next.js, Express | http-routes.md |
| Firestore | firebase-admin, runTransaction, batch 500, calendarBusyBlocks, composite index, security rules | firestore-backend.md |
| Supabase / Postgres | schema.sql, RLS, service role, optimistic concurrency, 23505, PostgREST 1000 rows | supabase-backend.md |
| Proving it | node:test, memory store, fake Google, TZ=, mutation check | testing.md, test-suite.md |
| Running it | health, stale, channel-expired, env vars, quotas, dashboards, runbook, ngrok, localhost | operations.md |
| Why it is built this way | audit, defect, kept deliberately, designed here, fix order | provenance.md |
Part of the Timerise Skills index, which lists the sibling skills.