Calendar Sync
Connect Google Calendar for on-demand two-way synchronisation — confirmed and pending bookings push out to your Google Calendar, and existing external events are reported back as a count of busy slots. Microsoft Outlook support is coming soon.
Overview
Calendar Sync connects your Google account so confirmed and pending TotalApp bookings can be pushed to your Google Calendar, and existing events on that calendar are reported back as a busy-slot count. Sync runs on demand — there is no background job — every time you click Sync Now, TotalApp requests a fresh Google access token and calls the real Google Calendar API v3.
Google Calendar
Connect via a Google Identity Services consent popup (not a full-page OAuth redirect). Grants a short-lived calendar.events-scoped token, requested fresh on each Connect/Sync click.
Microsoft Outlook
Coming soon. The Outlook card is shown for reference but Connect is disabled — Microsoft Graph sync is not implemented yet.
Outgoing + Incoming
Outgoing sync creates or updates one Google Calendar event per confirmed/pending booking. Incoming sync counts upcoming events on the calendar that TotalApp did not create. Each direction has its own toggle.
Sync Log
A rolling, in-memory log of recent sync operations shows success, error, and direction (outgoing / incoming) for every sync you trigger.
Connecting Google Calendar
- Navigate to Calendar Sync in the sidebar.
- On the Google Calendar card, click Connect Google Calendar.
- A real Google consent popup opens (Google Identity Services), requesting the
https://www.googleapis.com/auth/calendar.eventsscope. This is a genuine Google OAuth prompt, not a TotalApp-drawn modal. - After you approve, TotalApp receives a short-lived access token and immediately verifies it with a zero-op call to
/api/google-calendar/syncbefore marking the account as Connected. - The provider card updates to show the sync toggle controls and the last sync timestamp. The
primarycalendar is used — there is no calendar picker yet.
Token is not stored — you may see the popup again
Only a connected: true flag and your sync preferences are saved server-side (in appointments.json, via saveSyncToken). The actual Google access token is never persisted — it lives only in the browser for the duration of one Connect or Sync Now action. Each click requests a fresh token; Google will silently re-grant it without a popup if you're still within the same browser session, and will show the consent screen again once the token has expired or the session is gone.
Sync Directions
Outgoing Sync (TotalApp → Google Calendar)
When you click Sync Now with Outgoing Sync enabled, TotalApp fetches all bookings with status confirmed or pending and sends them to /api/google-calendar/sync. For each booking, the server looks for an existing event carrying that booking's ID in extendedProperties.private.totalappBookingId: if found, the event is patched (title, start/end time, notes); if not found, a new event is created. This makes repeated syncs idempotent — re-running Sync Now updates existing events rather than duplicating them.
| Booking Field | Google Calendar Event Field |
|---|---|
customerName | summary ("<name> — TotalApp booking") |
notes | description |
date + startTime / endTime | start.dateTime / end.dateTime |
id | extendedProperties.private.totalappBookingId (used to find the event on the next sync) |
Cancelling or deleting a booking in TotalApp does not currently update or remove the corresponding Google Calendar event — that event is left as-is until you clean it up manually.
Incoming Sync (Google Calendar → TotalApp)
When Incoming Sync is enabled, the server lists up to 50 upcoming events on the connected calendar and counts how many do not carry a totalappBookingId (i.e. were not created by TotalApp's outgoing sync). That count is shown in the Sync Log — for example "Imported 3 external event(s) as blocked slots". No booking records or blocked-slot entries are actually created in TotalApp from this count today; the availability engine does not yet read from Google Calendar, so external events do not block the public booking page. This is a reporting-only view of how many external events exist, not a working two-way block.
Sync Frequency
There is no background job, cron, or webhook. Sync only runs when a signed-in user clicks Sync Now on a connected provider card — everything in this document happens synchronously in that one request.
Sync Toggles
Each connected provider card shows two independent toggles:
| Toggle | On | Off |
|---|---|---|
| Outgoing Sync | Confirmed/pending bookings are pushed to Google Calendar on the next Sync Now click | Sync Now skips the outgoing step entirely |
| Incoming Sync | Sync Now also counts external events on the calendar | Sync Now skips the incoming step entirely |
Toggle changes are saved immediately via updateSyncToken — there is no Save button.
Sync Log
The Sync Log section at the bottom of the page shows the most recent sync operations in reverse chronological order. Each entry includes:
- Status icon — green check (success) or red triangle (error, e.g. the Google token request failed or expired).
- Message — how many bookings were pushed, or how many external events were counted.
- Direction badge — outgoing (blue) or incoming (amber).
- Provider — currently always
google. - Timestamp — local time of the sync event.
The log is kept in component state only (up to 50 entries) and resets on page reload — it is not persisted to the server.
Disconnecting Google Calendar
- Open the Google Calendar provider card.
- Click the Disconnect button (unlink icon, red outline hover).
- The sync preference record is deleted from
appointments.jsonand the provider card reverts to the unconnected state. - Events already created in Google Calendar by outgoing sync are not cleaned up automatically — delete them manually in Google Calendar if needed.
Revoke access at Google too
Disconnecting in TotalApp only removes the local sync preference — since no refresh token is stored server-side, there is nothing to revoke on TotalApp's end. If you want to fully revoke the granted calendar.events scope, visit your Google Account's Third-party apps & services page.
API Endpoints Reference
| Method | Endpoint | Purpose |
|---|---|---|
POST | /api/google-calendar/sync | Authenticated (app JWT required). Accepts { accessToken, calendarId, syncOutgoing, syncIncoming, bookings }, calls Google Calendar API v3 directly with the caller-supplied access token, and returns { outgoingCount, incomingCount }. Used both to verify a new connection (empty booking list, both directions off) and to perform a real Sync Now. |
GET/POST | /api/data/appointments | Generic tenant-scoped JSON store — holds resources, bookings, and syncTokens (the connection flag + preferences, no OAuth tokens). |
There is no separate OAuth authorize/callback route — the Google token is obtained entirely client-side via Google Identity Services (window.google.accounts.oauth2.initTokenClient) and forwarded to the server per-request, the same pattern already used by the Google Sheets and Google Drive integrations elsewhere in TotalApp.
Security Considerations
- The Google access token is never written to disk or to the appointments JSON store — it only exists in browser memory for the duration of a single Connect or Sync Now click, then is discarded.
- The
/api/google-calendar/syncendpoint requires a valid TotalApp app JWT (requireAppAuth) — an attacker without a signed-in session cannot call it, even if they somehow obtained a Google access token. - The requested scope is deliberately narrow:
https://www.googleapis.com/auth/calendar.eventsonly grants read/write access to events, not full calendar management (sharing, deleting whole calendars, etc.). - Because no refresh token is requested or stored, there is no long-lived credential to protect at rest — the tradeoff is that background/offline sync is not possible with the current design.