Welcome to the Jose Madrid Salsa developer docs — explore features, APIs, and deployment guides.
Jose Madrid SalsaJMS Docs

Google Calendar Two-Way Sync

OAuth-backed two-way sync between "Where is Jose?" events and the public Google Calendar

Google Calendar Two-Way Sync

Keeps the FeaturedEvent records flagged "Where is Jose?" and the company Google Calendar in agreement, in both directions. Managed from the Google Calendar card at the top of Admin → Events.

This is separate from the read-only Google Calendar integration, which fetches the same calendar with an API key and cannot write. Both can run at once; only this one needs OAuth.

Why it matters

The public /where-is-jose page renders the Google Calendar feed, not the database. Until an event reaches the calendar, flagging it "Where is Jose?" changes nothing a customer sees. Pushing to Google is the publish step.

What crosses, and what does not

Only events flagged isWhereIsJose are pushed. FeaturedEvent also holds the booking pipeline — shows applied to, waitlisted, or declined, along with booth fees — and the target calendar is public. Pushing everything would publish all of it.

DirectionWhat moves
Website → GoogleEvents flagged "Where is Jose?": title, description, location, dates
Google → WebsiteEvery event in the sync window, created as a flagged event
Website → GoogleRemoval, when an event is unflagged
Google → WebsiteNever a deletion. A removal on the calendar unlinks the pair only

A FeaturedEvent owns its staff, contacts, product manifest, and show financials. Deleting the calendar entry must not take a season of financials with it, so a remote deletion clears googleEventId and leaves the record standing.

Conflicts

An event is locally dirty when updatedAt > googleSyncedAt, and remotely dirty when Google's etag differs from the stored googleEtag. When both are true, the connection's conflict policy decides:

PolicyBehaviour
ASK (default)Neither copy is touched. The event is marked CONFLICT and listed in the admin card with Keep this site's / Keep Google's
LOCAL_WINSThe website's copy overwrites Google's
GOOGLE_WINSGoogle's copy overwrites the website's

Every sync write sets googleSyncedAt in the same update as the content change. If it lagged, its own write would bump updatedAt and every synced event would look dirty forever.

All-day events

FeaturedEvent.isAllDay is a stored column, not an inference. An all-day event's startDate sits on UTC midnight of the calendar date; Google speaks bare YYYY-MM-DD with an exclusive end, while we store an inclusive last day.

The column exists because the instant alone cannot carry the distinction once a UTC-hosted server and an Eastern-time office disagree about which moment is midnight.

Sync window

Each run reconciles one year back and two years forward. A bounded window rather than an incremental syncToken: the volume is a few dozen shows a year, and a sync token adds an expiry path exercised too rarely to stay correct.

Environment variables

No new variables. The integration reuses:

VariableDescription
GOOGLE_CLIENT_IDOAuth client, shared with sign-in and Google Business
GOOGLE_CLIENT_SECRETOAuth client secret
GOOGLE_CALENDAR_IDThe calendar to sync
NEXTAUTH_URLUsed to build the OAuth redirect URI

Setup

Enable the Calendar API

In Google Cloud Console, enable Google Calendar API for the same project that owns GOOGLE_CLIENT_ID.

Add https://www.googleapis.com/auth/calendar.events. The narrower calendar.events is deliberate — the sync writes events on one existing calendar and never manages calendars or reads ACLs.

Add the redirect URI

Add {NEXTAUTH_URL}/api/admin/events/google/callback to the OAuth client's authorised redirect URIs, for every environment that will connect.

Connect

In Admin → Events, press Connect on the Google Calendar card and grant access. Requires the events:sync-calendar permission.

Sync

Press Sync now. The card reports what went up, what came down, and anything that needs a decision.

If Google returns no refresh token the connection is refused rather than stored — it would expire within the hour and could not renew. This happens when the account has already granted these scopes; remove the app under the Google account's permissions and connect again.

Endpoints

RoutePurpose
GET /api/admin/events/google/statusConnection state and last-run counts
GET /api/admin/events/google/connectReturns the consent URL, sets the state cookie
GET /api/admin/events/google/callbackStores tokens, redirects back to Events
POST /api/admin/events/google/syncRuns one full reconciliation
PATCH /api/admin/events/google/settingsSets the conflict policy
POST /api/admin/events/google/resolveSettles one flagged conflict
POST /api/admin/events/google/disconnectDeletes the stored tokens

Disconnecting leaves both the calendar entries and the local googleEventId values in place, so reconnecting the same calendar resumes the pairs instead of duplicating shows.

Key files

FilePurpose
lib/events/google-sync-rules.tsWhich action a local/remote pair needs. Pure
lib/events/google-event-mapping.tsField translation both ways. Pure
lib/events/google-calendar-client.tsOAuth, token refresh, the four API calls
lib/events/google-calendar-sync.tsThe reconciler and conflict resolution
app/admin/events/_components/GoogleCalendarPanel.tsxThe admin card

The two pure modules carry the logic that is expensive to get wrong and are tested directly, without a network.

How is this guide?

Edit on GitHub

Last updated on

On this page