The problem
What problem does Google Calendar sync solve?
"Connect your Google Calendar" is one button on a settings page. Behind it, a booking system has to know, at the moment a client picks a time, that the host has a dentist appointment, and it has to put the new booking into the host's calendar so nobody books over it from the other side.
The button is the easy part. The hard part is what happens when Google sends five notifications for one change, when a calendar has more events than one page holds, when an all-day holiday falls on a different day in the server's time zone, when the watch channel expires on a Saturday night, or when the host revokes access and nobody notices. Get one of those wrong and the calendar shows a host free while they sit in another meeting, or busy all week for no reason.
This skill gives your coding agent the whole sync, written by the engineers who build calendar integrations for booking systems: the OAuth connect, busy time from the calendars a host chooses, push notifications with channel renewal, booking events with Google Meet links, and health per calendar so a broken connection becomes a reconnect prompt. You own the code, run it on Next.js on Vercel, with Supabase or Firestore, under your own Google Cloud project.
The module
What does the skill build?
An agent with this skill builds one module for a Next.js App Router app, on your stack. For Google Calendar sync that module consists of:
- 1. OAuth with PKCE
- 2. Busy time from the calendars a host chooses kept current by push notifications and make-before-break channel renewal
- 3. Confirmed bookings written as idempotent events with Google Meet links
- 4. Derived health per calendar
- 5. An Express adapter beside the App Router routes
Provenance
Where do the rules come from?
This skill was written by the engineer who owns the calendar integration of a booking API, and audited against the earlier implementation of that integration together with the slot engine that consumes its output. The templates hold the properties a calendar sync has to hold: a booking's own event never becomes busy time; a burst of notifications converges to one block per event; every page of a calendar is read, and a failed page deletes nothing; a multi-day all-day event blocks every day, whatever the server's time zone; a channel is renewed before the old one is stopped; a booking pushed twice is one event; revoked access becomes a reconnect prompt that keeps every setting. 32 tests state them, run under four process time zones, and `references/provenance.md` has the record.
The engineering ledger of this skill, for the person editing it. The skill was written by the engineer who owns the calendar integration of a booking API, where hosts connect their calendars to block availability and receive bookings as events, and audited against the earlier implementation of that integration. The earlier implementation had already been hardened once: single-use OAuth state with PKCE, encrypted tokens, HMAC-signed channel tokens bound to their channel, persisted token refreshes, and reconnect clean-up that no longer failed the callback. Those are carried over as they were.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed in the templates
- A booking's own event blocked the rest of its session Events written for bookings were tagged with the booking id, but busy sync never looked at the tag. On a calendar with both syncs on, the first booking of a group session came back seconds later as a busy block of unlimited quantity, and the engine (which deliberately ignores the host's bookings of the same service) still hid the remaining seats. Shipped: events carrying the private marker are skipped, in busy-sync.md.
- Renewal stopped the old channel before opening the new one If the new watch call failed, the calendar was left with no channel and no channel record, so the cron never found it again. Busy sync stayed "on" in the UI while its blocks froze. Shipped: new before old, the old kept on failure, and the cron opens a channel for any busy calendar without one, in channels-and-settings.md.
- Busy time froze on quiet calendars The window was
[now, now + 180 days], but it only moved when a notification arrived. A calendar nobody edited for a month never imported events entering the window, and missed notifications were never repaired. Shipped: the cron resyncs every busy calendar. - Concurrent notifications left phantom busy blocks Busy blocks had random ids. Two syncs started by one burst both saw an event as new and both created a block. The next sync keyed blocks by event id in a map, so one duplicate was updated and deleted with the event while the other stayed forever, blocking time the host had freed. Shipped: deterministic ids, a per-calendar lease with a queued rerun, and a diff that deletes every stored row not desired.
- Multi-day all-day events blocked only their first day All-day events were mapped from
start.dateto 23:59:59.999 of the same day;end.datewas ignored. A week's vacation blocked Monday. Shipped:start.dateto the exclusiveend.date, at local midnights of the calendar's zone, correct on DST days.
Show 9 moreShow fewer
- Only the first 2500 events were read
events.listwas called once withmaxResults: 2500;nextPageTokenwas ignored, and the diff then deleted the blocks of every event past the first page. Shipped: every page, and any failure aborts before deleting, in google-api.md. - Free and declined events blocked Events marked Free (most all-day events by default) and invitations the host declined were imported as busy. Shipped: both skipped, except on holiday calendars.
- Shared calendars could not receive bookings, and new rows looked read-only Only
ownercounted as writable, thoughwritercan insert events. Separately, a calendar's first listing storedisWritable: falseregardless of its role, so the booking-sync toggle was unavailable until the list was loaded a second time. Shipped:ownerorwriter, correct on first listing, and a server-side guard on enabling booking sync. - A revoked grant looked like "no calendars" Every listing error returned an empty list, nothing recorded
invalid_grant, and renewals failed twice a day in the logs only. Shipped:reauth_requiredon the connection, surfaced through health and the list route. - Reconnecting wiped every setting Re-authorizing (the fix for a revoked grant) deleted all calendar rows and busy blocks, even for the same Google account. Shipped: the account e-mail identifies the account; the same account keeps its rows and gets fresh channels, a different one is torn down.
- Booking events were not idempotent Events got Google-generated ids. A repeated trigger, or the backfill when booking sync was switched on (which de-duplicated only against the 180-day window), created a second event. Shipped: the event id is derived from the booking id; 409 becomes an update, in booking-events.md.
- Smaller ones could push it past Google's 256-character limit. It now holds only the channel id.
- The second Next.js route did not compile on its own Found by the 0.2.0 release check, not by the audit.
app/api/calendar/google/calendars/[calendarId]/route.tsshared a code block with the[action]route and usedcalendarRouteswithout importing it, so the file an agent copied failedtsc. Shipped: its own block with its own import, in http-routes.md; the release check writes every block to its own path and compiles it. - The wiring failed next build without secrets Found by the 0.2.0 agent evals, not by the audit.
setup.tsread every variable at import, andnext buildimports each route module to collect page data, so a build without the secrets (CI, a preview deployment) failed withCALENDAR_... is not set; every agent wrapped it in a lazy getter of its own. Shipped:calendarRoutes()andcalendarDeps()build on first call, in http-routes.md; the release check runsnext buildwith an empty environment.
Non-negotiables
What are the 5 rules the module never breaks?
Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.
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. The connect tests consume a state twice, let one expire, and check the user comes from the server.
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. The webhook test sends a forged token, a foreign resource and an unknown channel.
Never stop the old channel before the new one exists.
If the new watch fails, the calendar has no channel and its busy time stops updating while the UI says "on". The renewal tests fail the new watch and expect the old channel kept, and remove a channel and expect the cron to open one.
Never store tokens in clear text.
A refresh token is long-lived read and write access to a calendar. The token test round-trips AES-256-GCM and rejects tampering; the connect test reads the stored tokens back encrypted.
Never let a Google failure fail a booking.
Outbound writes log and return; the booking stands.
pushBookingEventandremoveBookingEventcatch every Google error and return a result per calendar, as booking-events.md shows.
Fit
When should you use it, and when not?
Use it for
- 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.
Not for
- Outlook and Microsoft 365 calendarsInstead
microsoft-calendar-sync - Generating bookable slots and subtracting busy timeInstead
time-slot-generator - Public booking pages with Google and Outlook sync built inInstead
calendar-scheduling-links - A calendar UI or invitation e-mailsInsteadThe host's own UI and e-mail
- Availability from a whole Workspace without per-user OAuthInsteadGoogle's free/busy query with domain-wide delegation, not templated here
Build it yourself
How do I install it?
One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.
$ npx skills add timerise-ai/google-calendar-syncClaude Code
Invoke with /google-calendar-sync
Codex CLI
Invoke with $google-calendar-sync
Gemini CLI
Invoke with /skills
Name the agents instead with -a, for example npx skills add timerise-ai/google-calendar-sync -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.
What is inside the repository (17 entries)
SKILL.mdEntry point: trigger, architecture, adaptation contract, critical facts, hard rules, quick start, reference directoryreferences/adaptation.mdHost prerequisites, seam contract, order of work, the non-negotiablesreferences/data-model.mdRecords, types, the store contract, the rename tablereferences/oauth-and-tokens.mdGoogle Cloud setup, scopes, the OAuth round trip, token lifetime, encryptionreferences/google-api.mdThe fetch-based client and the API facts that shape itreferences/busy-sync.mdWhich events block, the sync run, feeding availabilityreferences/booking-events.mdWriting and deleting booking events, Meet linksreferences/channels-and-settings.mdWatch channels, renewal, settings transitions, the calendar listreferences/http-routes.mdRoute handlers, the webhook, Next.js and Express wiringreferences/firestore-backend.mdCollections, index, rules, storereferences/supabase-backend.mdSchema, RLS, optimistic concurrency, storereferences/operations.mdHealth, logs, configuration, runbookreferences/testing.mdRunning the suite, what it pins, mutation check, doublesreferences/test-suite.mdThe 32 testsreferences/provenance.mdThe audit ledger: fixed, kept deliberately, changed, designed hereevals/prompts.mdwith what an operator types, and one file per agent eval run.github/workflows/agent-eval.ymlRuns the agent evals on every published release
Recent releases
- v0.2.2October 16, 2026
Wording release, from scoring the prompt-1 agent eval runs against 0.2.1.
- v0.2.1October 16, 2026
Fix release, from scoring the prompt-1 agent eval runs against 0.2.0.
- v0.2.0October 16, 2026
The skill brought to the Timerise skill standard, with a new adaptation reference, agent evals, and one template fix found by compiling every block on its own.
After installing
What do I tell my agent?
Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.
Our staff should be able to connect their Google Calendar in this Next.js app. Events on the calendars they pick must block their availability and stay up to date without anyone pressing refresh, and every confirmed booking should land in their calendar, with a Google Meet link if they want one. We use Supabase.
Add Google Calendar sync for our instructors on Firestore. We run group classes: when a student books a class it goes into the instructor's calendar, but the class must stay open for the other students.
FirestoreAudit our Google Calendar integration: hosts show as busy at times they cleared weeks ago, some calendars stopped updating without any error, and people who revoked access just see an empty calendar list.
Tested
How does it do in each agent?
We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.
- Built, checks pass
Gemini CLI0.64.0
gemini-3.8-flash
Our staff should be able to connect their Google Calendar in this Next.js app. Events on the calendars they pick must block their availability and stay up to date without anyone pressing refresh, and every confirmed booking should land in their calendar, with a Google Meet link if they want one. We use Supabase.
Typecheck: passBuild: passTests: pass- Time
- 11 min
- Changed
- 28 files, +3,349 lines
- Stack
- Supabase
- Skill
- v0.2.2
- Run
- Oct 16, 2026
What we saw
Rubric 8/8. Scored from the summary only; item 2 is held on its word.
setup.tsbuilds on first call and checks the session withauth.getUser, the cron route answers GET and POST as shipped, the suite runs 32 tests in each of four zones, and the final message ends with all six handover items, naming the two placeholder seams and the booking hooks to call. - Built, checks pass
Codex CLIcodex-cli 0.163.0
gpt-6.1-sol
Our staff should be able to connect their Google Calendar in this Next.js app. Events on the calendars they pick must block their availability and stay up to date without anyone pressing refresh, and every confirmed booking should land in their calendar, with a Google Meet link if they want one. We use Supabase.
Typecheck: passBuild: passTests: pass- Time
- 3 min
- Changed
- 37 files, +4,145 lines
- Stack
- Supabase
- Skill
- v0.2.2
- Run
- Oct 16, 2026
What we saw
Rubric 8/8. Scored from the transcript. It installed
@supabase/supabase-jsand@supabase/ssrfrom the registry, extracted every block verbatim and derivedsetup.tsfrom the shipped one by swapping the store, the host seams anddefer. The suite runs unchanged in four zones, 32 each; it also ran the ten mutations of the testing table against temporary copies, all detected, leaving the templates untouched. Supabase sessions are verified,.env.examplelists the eight variables, the cron is 12-hourly, and the handover has all six items, the sixth saying no placeholder seam remains. - Built, checks pass
Claude Code2.1.301
claude-opus-5-5
Our staff should be able to connect their Google Calendar in this Next.js app. Events on the calendars they pick must block their availability and stay up to date without anyone pressing refresh, and every confirmed booking should land in their calendar, with a Google Meet link if they want one. We use Supabase.
Typecheck: passBuild: passTests: pass- Time
- 5 min
- Changed
- 31 files, +3,674 lines
- Stack
- Supabase
- Skill
- v0.2.2
- Run
- Oct 16, 2026
What we saw
Rubric 8/8. Scored from the summary.
setup.tsbuilds on first use and the build passes with no variables set; the host seams sit incalendar-sync/host.tsbeside it, withlistUpcomingBookingsForHostandsaveMeetingUrlleft as named placeholders rather than an invented bookings table.authenticatechecks the Supabase session or Bearer token with Supabase, the suite runs as shipped (32 in four zones), the cron is 12-hourly invercel.json, and the final message ends with all six handover items, the placeholders among them.
Build it with Timerise
How long does it take, and what does it cost?
We quote this module per project. The price depends on what it has to connect to. The path to a number is short and free:
Step 1
Brief
Tell us what the module must connect to. Takes minutes, in a chat.
Step 2
Prototype in 48 hours
A clickable prototype of your system and a quote, at no cost.
Step 3
Build and handoff
One project price. Source code, documentation and IP are yours.
Two ways to get Google Calendar sync
Build it yourself
Install the skill. Your own agent builds the module.
- MIT licensed, no strings
- Runs in Claude Code, Codex CLI and Gemini CLI
- The same rules our engineers build by
$ npx skills add timerise-ai/google-calendar-syncBuild it with Timerise
Send a brief. We build Google Calendar sync into a system you own.
- Clickable prototype and a quote within 48 hours, free
- One project price, no subscription, no commission
- Source code, documentation and IP handed over
Generated from the skill's own files at commit 579089c. Every rule above links to where the repository says it. All skills