docs/long-term-plans/google-calendar-provider-design.md
Status: Planned
Add a Google Calendar provider (GOOGLE_CALENDAR) to Super Productivity with two-way event sync via the Google Calendar REST API. Authentication uses a hybrid approach: an auth proxy by default with an option for user-provided OAuth credentials.
| Decision | Choice | Rationale |
|---|---|---|
| Integration level | Two-way event sync (phased) | Full value requires writing back status changes |
| Auth approach | Hybrid: auth proxy default + user-provided option | Best UX for most users; self-hosters/privacy users can opt out of proxy |
| API layer | Google Calendar REST API v3 | Better documented, more reliable than Google's CalDAV endpoint; avoids CalDAV quirks |
| Initial sync direction | Google → SP first, status sync back | Reduce scope for first version; no SP → Google event creation yet |
| Auth proxy hosting | Decide at implementation time | Proxy is stateless and thin enough to move between hosting options |
| Token storage | Reuse existing SyncCredentialStore (IndexedDB sup-sync) | Proven pattern from Dropbox sync |
Google Calendar API requires OAuth 2.0. No alternative exists — even Google's CalDAV endpoint requires OAuth. This is the core complexity of the feature.
client_secret embedded in source is public. Desktop/mobile clients are treated as "public clients" (PKCE, no secret), but web clients traditionally need one.Default mode (proxy): A stateless auth proxy handles OAuth token exchange, keeping client_secret server-side. All platforms use the same proxy. Calendar data never touches the proxy — only OAuth tokens during exchange/refresh.
Custom mode: Users provide their own Google Cloud OAuth credentials. The app performs PKCE flows directly with Google. Intended for self-hosted instances and privacy-conscious users.
The proxy is intentionally minimal — stateless, no database, no sessions, no user accounts.
Endpoints:
POST /auth/google/token-exchange
Input: { code, code_verifier, redirect_uri, platform }
Action: Exchanges auth code for tokens using client_secret
Output: { access_token, refresh_token, expires_in }
POST /auth/google/token-refresh
Input: { refresh_token }
Action: Refreshes access token using client_secret
Output: { access_token, expires_in }
Hosting options (to be decided):
1. Client generates PKCE code_verifier + code_challenge
2. Client opens Google consent screen URL (with code_challenge)
3. Google redirects to proxy with auth code
4. Proxy exchanges code for tokens (using client_secret + code_verifier)
5. Proxy redirects to app with tokens:
- Electron: custom protocol (super-productivity://oauth/google)
- Web: redirect to app origin
- Android/iOS: deep link (com.super-productivity.app://oauth/google)
6. Client stores tokens locally in SyncCredentialStore
7. Client calls Google Calendar API directly (proxy not involved)
8. On 401: client calls proxy /token-refresh for new access_token
| Platform | Proxy mode | Custom credentials mode |
|---|---|---|
| Electron | Proxy → custom protocol redirect | Local loopback server (http://127.0.0.1:<port>/callback) |
| Web/PWA | Proxy → redirect to app origin | Standard redirect (user registers their own origin) |
| Android | Proxy → deep link | Deep link with user's own client ID |
| iOS | Proxy → deep link / universal link | Deep link with user's own client ID |
Located at src/app/core/oauth/ — designed to support future providers (Outlook, etc.):
src/app/core/oauth/
google-oauth.service.ts # Google-specific OAuth config + PKCE flow
oauth-proxy.service.ts # Routes token exchange through auth proxy
oauth-credential.store.ts # Extends/reuses SyncCredentialStore
New provider at src/app/features/issue/providers/google-calendar/:
google-calendar/
google-calendar.model.ts # GoogleCalendarCfg, event type mappings
google-calendar.const.ts # Scopes, API URLs, defaults
google-calendar-api.service.ts # REST API calls (events CRUD, calendar listing)
google-calendar.service.ts # Extends BaseIssueProviderService
google-calendar-sync-adapter.ts # Two-way sync logic
google-calendar-cfg/ # Settings UI component
interface GoogleCalendarCfg extends BaseIssueProviderCfg {
calendarIds: string[];
authMode: 'proxy' | 'custom';
customClientId?: string;
customClientSecret?: string;
syncDirection: 'read-only' | 'two-way';
checkUpdatesEvery: number;
}
| Google Calendar Event | Super Productivity |
|---|---|
summary | Task title |
description | Task notes |
start / end | CalendarIntegrationEvent start / duration |
status (confirmed/cancelled) | Task done state |
updated | Last-modified timestamp for sync |
| Super Productivity | Google Calendar Event |
|---|---|
| Task marked done | Event status → cancelled (or configurable) |
| Title changed | summary updated |
| Notes changed | description updated |
Follows the pattern established by CaldavSyncAdapterService:
syncToken — only returns events changed since last sync, much more efficient than full re-fetchupdated timestamps (last-write-wins, server as authority)CalendarIntegrationEvent (like existing ICAL provider)syncTokenupdated timestampssrc/app/features/issue/providers/caldav/caldav-sync-adapter.service.tssrc/app/op-log/sync-providers/file-based/dropbox/dropbox.tssrc/app/op-log/sync-providers/credential-store.service.tsdocs/long-term-plans/calendar-two-way-sync-technical-analysis.md